利用java的atomic包下工具类的CAS功能手写一个读写锁并附带乐观锁功能

先一步一步来。

首先就是明白读写锁的功能:

读锁和读锁------不排斥

写锁和写锁------排斥

读锁和写锁------排斥

我们先定义一些属性:

//读写互斥,读读不互斥,写写互斥
private AtomicStampedReference<Thread> reference=new AtomicStampedReference<>(null,1);
private AtomicStampedReference<Thread> id=new AtomicStampedReference<>(null,1);

读锁和读锁------不排斥:

所以在写的时候,读锁的判定条件应该是二个或二个以上,这样保证,第一个线程通过条件能进来的同时,其他线程也能通过别的条件进来。

public void readLock(){
    Thread thread=Thread.currentThread();
    while ((!id.compareAndSet(null,null,id.getStamp(),id.getStamp()+1))
    &&(!reference.compareAndSet(null,thread, 1,1))){
        //循环堵塞
    }
}

可以看到,这里的while判定条件我用了&&符号,保证了读锁的并发性。

id.compareAndSet(null,null,id.getStamp(),id.getStamp()+1)   他只有值为null的时候才为true

reference.compareAndSet(null,thread, 1,1)    他只有值为null的时候才会为true

写锁和写锁------排斥

读锁和写锁------排斥

public void writeLock(){
    Thread thread=Thread.currentThread();
    while (!reference.compareAndSet(null,thread,reference.getStamp(),1)
    ||!id.compareAndSet(null,thread,1,1)){
        //循环堵塞
    }
}

可以看到,这里的while判定条件我用了||符号,保证了写锁的互斥性。

id.compareAndSet(null,null,id.getStamp(),id.getStamp()+1)   他只有值为null的时候才为true

reference.compareAndSet(null,thread, 1,1)    注意这里:他只有值为null,并且他的版本号为1的时候才为true

我们可以看到一旦读锁(或者写锁)先执行了,那么其他线程的写锁(或者读锁)就会陷入死循环。

那么读锁(或者写锁)执行完了,该怎么开始执行写锁(或者读锁)呢?

那么我们就可以推断:

如果读锁先执行,需要给写锁放行的是:

id 的版本号为1,且reference的值为null

public void unReadLock(){
    reference.compareAndSet(reference.getReference(),null,reference.getStamp(),1);
    id.compareAndSet(reference.getReference(),null,id.getStamp(),id.getStamp()-1);
}

id.compareAndSet(null,null,id.getStamp(),id.getStamp()-1);

请注意这里,因为读锁是并发的,所以我们希望执行了几个读锁就要解开几个读锁,写锁才能执行。

如果写锁先执行,需要给写锁和读锁都放行:

id 的版本号为1,且reference的值不为null

public void unWriteLock(){
    reference.compareAndSet(reference.getReference(),null,1,1);
    id.compareAndSet(id.getReference(),null,1,1);
}

id.compareAndSet(id.getReference(),null,1,1);

写锁时互斥的,所以直接把他变成初始值就行了。

好,上面写完后,我们就实现了一个读写锁,读和写是互斥的,所以他是一种悲观锁。

悲观锁完成之后,我们希望还要加一个乐观锁。

首先明白乐观锁并不是一种锁,所以它不需要解锁,还要明白他的存在是解决读锁和写锁互斥的问题。

比方说,我们一直在读,那么可能负责写的这个线程就一直没有机会执行。这是非常不好的事情。

既然,乐观锁不和写锁互斥,我们该怎么保证数据安全呢?

答案是判断,如果写操作发生,我们就加上读锁,如果写没有发生,就不加读锁。

开始:先加一些属性:

private AtomicInteger ifWrite=new AtomicInteger(0);
private volatile int stamp;

ifWrite   用于判断写锁有没有发生

stamp   用于及时更新ifWrite的状态

写锁一执行,我们就改变 ifWrite

public void writeLock(){
    Thread thread=Thread.currentThread();
    while (!reference.compareAndSet(null,thread,reference.getStamp(),1)
    ||!id.compareAndSet(null,thread,1,1)){
        //循环堵塞
    }
    ifWrite.compareAndSet(ifWrite.get(),99);
}

 只要写锁执行了,我们就弃用乐观锁使用读锁,在读锁关的时候,再把状态该回去。

public void unReadLock(){
    reference.compareAndSet(reference.getReference(),null,reference.getStamp(),1);
    id.compareAndSet(reference.getReference(),null,id.getStamp(),id.getStamp()-1);
    ifWrite.compareAndSet(ifWrite.get(),0);
}

那么好了,我们的乐观锁只需要保证获取到ifWrite赋值给stamp就行了。

public void OptimisticReadLock(){
    this.stamp=ifWrite.get();
}

public Boolean validate(){
    if(this.stamp==99)
        return true;
    return false;
}

好了,完整代码就是:

public class XjggLock {
    private AtomicStampedReference<Thread> reference=new AtomicStampedReference<>(null,1);
    private AtomicStampedReference<Thread> id=new AtomicStampedReference<>(null,1);
    private AtomicInteger ifWrite=new AtomicInteger(0);
    private volatile int stamp;
    public void readLock(){
        Thread thread=Thread.currentThread();
        while ((!id.compareAndSet(null,null,id.getStamp(),id.getStamp()+1))
                &&(!reference.compareAndSet(null,thread, 1,1))){
            //循环堵塞
        }
    }
    public void unReadLock(){
        reference.compareAndSet(reference.getReference(),null,reference.getStamp(),1);
        id.compareAndSet(reference.getReference(),null,id.getStamp(),id.getStamp()-1);
        ifWrite.compareAndSet(ifWrite.get(),0);
    }
    public void writeLock(){
        Thread thread=Thread.currentThread();
        while (!reference.compareAndSet(null,thread,reference.getStamp(),1)
                ||!id.compareAndSet(null,thread,1,1)){
            //循环堵塞
        }
        ifWrite.compareAndSet(ifWrite.get(),99);
    }
    public void unWriteLock(){
        reference.compareAndSet(reference.getReference(),null,1,1);
        id.compareAndSet(id.getReference(),null,1,1);
    }
    public void OptimisticReadLock(){
        this.stamp=ifWrite.get();
    }
    public Boolean validate(){
        if(this.stamp==99)
            return true;
        return false;
    }
}

代码很少,很简单。

我们来测试一下:

读并发:

class Test{
    private static XjggLock xjggLock=new XjggLock();
    private static int num;
    static void read(){
        xjggLock.readLock();
        try {
            int n=num;
            System.out.println(Thread.currentThread().getName()+"===>读执行");
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName()+"===>读结束");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            xjggLock.unReadLock();
        }
    }
    public static void main(String[] args) {
        //测试读能并发
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                read();
            }).start();
        }
    }
}

结果:可以看到没有问题

Thread-1===>读执行
Thread-2===>读执行
Thread-0===>读执行
Thread-0===>读结束
Thread-2===>读结束
Thread-1===>读结束

写互斥:

class Test{
    private static XjggLock xjggLock=new XjggLock();
    private static int num;
    static void write(){
        xjggLock.writeLock();
        try {
            num+=99;
            System.out.println(Thread.currentThread().getName()+"===>写执行");
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName()+"===>写结束");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            xjggLock.unWriteLock();
        }
    }
    public static void main(String[] args) {
        //测试写不能并发
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                write();
            }).start();
        }
    }
}

结果:可以看到没有问题

Thread-0===>写执行
Thread-0===>写结束
Thread-2===>写执行
Thread-2===>写结束
Thread-1===>写执行
Thread-1===>写结束

读写互斥:

class Test{
    private static XjggLock xjggLock=new XjggLock();
    private static int num;
    static void read(){
        xjggLock.readLock();
        try {
            int n=num;
            System.out.println(Thread.currentThread().getName()+"===>读执行");
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName()+"===>读结束");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            xjggLock.unReadLock();
        }
    }
    static void write(){
        xjggLock.writeLock();
        try {
            num+=99;
            System.out.println(Thread.currentThread().getName()+"===>写执行");
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName()+"===>写结束");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            xjggLock.unWriteLock();
        }
    }
    public static void main(String[] args) {
        //测试读写不能并发
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                read();
            }).start();
        }
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                write();
            }).start();
        }
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                read();
            }).start();
        }
    }
}

结果:可以看到没有问题

Thread-0===>读执行
Thread-4===>读执行
Thread-1===>读执行
Thread-5===>读执行
Thread-1===>读结束
Thread-0===>读结束
Thread-4===>读结束
Thread-5===>读结束
Thread-3===>写执行
Thread-3===>写结束
Thread-2===>写执行
Thread-2===>写结束

再测试测试乐观锁:

class Test{
    private static XjggLock xjggLock=new XjggLock();
    private static int num;
    static void read(){
        xjggLock.OptimisticReadLock();//加一个乐观锁
        if(xjggLock.validate()){
            xjggLock.readLock();
            System.out.println("当前写操作已发生,为了安全,我们加上读锁,此时写锁不可以进来。");
            try {
                int n=num;
                System.out.println(Thread.currentThread().getName()+"===>读执行,写锁不可以进来");
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName()+"===>读结束,写锁不可以进来");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                xjggLock.unReadLock();
            }
        }else{
            int n=num;
            System.out.println("当前没有发生写的操作,我们可以直接取值,此时写锁可以进来。");
            System.out.println(Thread.currentThread().getName()+"===>读执行,写锁可以进来");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"===>读结束,写锁可以进来");
        }
    }
    static void write(){
        xjggLock.writeLock();
        try {
            num+=99;
            System.out.println(Thread.currentThread().getName()+"===>写执行");
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread().getName()+"===>写结束");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            xjggLock.unWriteLock();
        }
    }
    public static void main(String[] args) {
        //测试乐观锁
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                read();
            }).start();
        }
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                write();
            }).start();
        }
    }
}

结果:可以看到没有问题

当前没有发生写的操作,我们可以直接取值,此时写锁可以进来。
Thread-1===>读执行,写锁可以进来
Thread-3===>写执行
Thread-1===>读结束,写锁可以进来
Thread-3===>写结束
当前写操作已发生,为了安全,我们加上读锁,此时写锁不可以进来。
Thread-0===>读执行,写锁不可以进来
Thread-0===>读结束,写锁不可以进来
Thread-2===>写执行
Thread-2===>写结束

总结一下就是,读写锁,可以提高读的效率,还保证了数据的安全性,而加入乐观锁保证了读的时候也可以写,进一步提升了执行效率,再配合读锁,也保证了数据的安全性。

当然就这几行代码想把读写锁写的很完美是不现实的,可能它会存在一些问题,虽然我还没测试出什么问题,但是它应该是不可信的!!!

像这么好用的锁,JDK肯定是会有的。JDK8开始新增锁(java.util.concurrent.locks.StampedLock)所以在业务中我们直接用这个类就行了。

希望对你们有帮助!

猜你喜欢

转载自blog.csdn.net/xiaomaomixj/article/details/126852372