并发编程(12):ReentrantReadWriteLock 与 StampLock的基本使用与实现原理以及二者的区别。

1、ReentrantReadWriteLock (可重入读写锁) 

      1.1、在前面我们剖析了ReentrantLock(可重入锁),其实现是使用了AQS同步器来实现的,我们知道ReentrantLock是以独占的方式来实现锁互斥的,也就是说,当去获取锁的时候,如果获取成功就会将当前锁的独占线程设置为当前线程,在锁被占有阶段当其他线程来申请锁的时候,就会被加入到AQS的同步队列中进行等待,当持有锁的线程释放锁,就会去唤醒同步队列中的head节点的next节点的线程,让当前线程重新去申请锁,如果不考虑其他因素,是一定能够申请到锁的。这个是ReentrantLock的实现原理。

      1.2、那么什么是ReentrantReadWriteLock,从字面的意思来看就是可重入的读写锁。它分为两把锁 读锁写锁,且都是可重入的。

              读锁跟写锁的互斥特性:

                     读锁 跟 读锁 :是不互斥的,如果多条线程同时读取共享资源,那么是不会发生线程阻塞的。

                     读锁 跟 写锁 :是互斥的,也就是说当有线程持有读锁,此时去申请写锁的线程将会被阻塞,同理,当有线程持有                                                  写锁,如果此时有线程去申请读锁也会被阻塞。

                     写锁 跟 写锁 :是互斥的,当有线程持有写锁,那么再去申请的线程就会被阻塞。

 

     1.3、ReentrantReadWriteLock的实现原理:

              我们知道ReentrantLock的实现中,获取锁失败的线程会被构建节点node加入的AQS的通过不队列中,且node的mode(模式)是EXCLUSIVE(独占),其实ReentrantReadWriteLock也是使用AQS的同步队列来实现的,且申请写锁的线程也是构建独占的node加入到AQS的同步队列,但是申请读锁的线程是SHARED共享模式的node加入到同步队列中的。   

    ReentrantReadWriteLock 的队列唤醒规则:

         1、当ReentrantReadWriteLock 的等待队列队首结点是共享结点,说明当前写锁被占用,当写锁释放时,会以传播的方式唤醒头结点之后紧邻的各个共享结点。

         2、当ReentrantReadWriteLock 的等待队列队首结点是独占结点,说明当前读锁被使用,当读锁释放归零后,会唤醒队首的独占结点。

     1.4、ReentrantReadWriteLock的弊端:

               在上面我们分析ReentrantReadWriteLock的基本规则与实现方式,我么会发现一个问题,那就是在非公平的环境中读多写少的情况下,写线程会发生线程饥饿,什么意思呢?我们假设有一个ReentrantReadWriteLock读写锁,当前是读锁被占有的,且有一个申请写锁的线程被阻塞,因为读锁是不互斥的,这个时候大量的读操来读取数据,这个时候就会造成那一条申请写锁的线程会一直被阻塞,这就造成了写线程的饥饿。因此在jdk1.8的时候,提供了一个StampLock来解决这个问题。

 

2、StampLock

      在讲StampLock之前我们先讲一下锁升级\锁降级

       2.1、什么是锁的升降级?

           锁升级:读锁 --> 写锁,意思就是一条线程在不释放读锁的情况下去申请写锁,如果申请到了,再将读锁释放掉,当前线程就从获取到读锁升级到了写锁。 支持锁升级的锁有:StampLock。

           锁降级:写锁-->读锁,意思就是一条线程在不释放写锁的情况下,去申请读锁,如果申请到了,再将写锁释放掉,那么当前线程就顺利的从写锁降级到读锁了。支持锁降级的锁有:ReentrantReadWriteLock、StampLock。

       2.2、什么是StampLock?            

          StampLock在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。在上面我们发现了ReentrantReadWriteLock会产生写线程的饥饿问题,因此StampLock就是为了优化ReentrantReadWriteLock写线程饥饿问题而产生的。

       2.3、StampLock的特点:

            1、所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
            2、所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
            3、StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
            4、StampedLock有三种访问模式:
                         ①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
                         ②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
                         ③Optimistic reading(乐观读模式):这是一种优化的读模式。
            5、StampedLock支持读锁和写锁的相互转换我们知道RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不                      能直接升级为写锁的。StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
            6、无论写锁还是读锁,都不支持Conditon等待

       2.4、StampLock的使用案例(我们使用官网的案例):

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    public void move(double deltaX, double deltaY) {
        使用写锁-独占操作,并返回一个邮票
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            使用邮票来释放写锁
            sl.unlockWrite(stamp);      
        }
    }

    
    使用乐观读锁访问共享资源

    注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其 
    他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是 
    最新的数据,但是一致性还是得到保障的。
    public double distanceFromOrigin() {
        使用乐观读锁-并返回一个邮票,乐观读不会阻塞写入操作,从而解决了写操作线程饥饿问题。
        long stamp = sl.tryOptimisticRead();    

        拷贝共享资源到本地方法栈中
        double currentX = x, currentY = y;      
        if (!sl.validate(stamp)) {              
            
            如果验证乐观读锁的邮票失败,说明有写锁被占用,可能造成数据不一致,
            所以要切换到普通读锁模式。
            stamp = sl.readLock();             
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        如果验证乐观读锁的邮票成功,说明在此期间没有写操作进行数据修改,那就直接使用共享数据。
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }


    锁升级:读锁--> 写锁
    public void moveIfAtOrigin(double newX, double newY) { // upgrade
        // Could instead start with optimistic, not read mode
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                读锁转换为写锁
                long ws = sl.tryConvertToWriteLock(stamp); 
                if (ws != 0L) {
                    如果升级到写锁成功,就直接进行写操作。
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    //如果升级到写锁失败,那就释放读锁,且重新申请写锁。
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            //释放持有的锁。
            sl.unlock(stamp);
        }
    }


}


     2.5、StampLock的实现原理:

              上面我们提到了ReentrantReadWriteLock的实现方式是使用AQS的同步队列来实现的,在队列中write 锁的节点node类型是独占(EXCLUSIVE),read 锁的节点node类型是共享(SHARED), 如果唤醒的write锁的节点,只会唤醒当前一个write 锁的节点,当唤醒的是一个read锁的节点就会逐个唤醒后续的read锁节点(跟CountDownLatch的传递性唤醒是一个道理),直到又碰见一个write锁节点为止。

              StampLock的实现原理也是使用了AQS的同步队列来实现的,但是多个read锁节点相邻的时候,它并不是放入到AQS的同步队列中,而是会有一个cwait的节点用来存放相邻的read锁的节点,唤醒的时候则唤醒AQS中的当前read锁节点+cwait的所有read锁节点。这就是StampLock的实现原理与ReentrantReadWriteLock的实现原理的区别。示意图如下:

                  

               

        2.6、StampedLock的等待队列与ReentrantReadWriteLock的AQS同步队列相比,有以下特点:

                  1、 当入队一个线程时,如果队尾是读结点,不会直接链接到队尾,而是链接到该读结点的cowait链中,cowait链本质是一个栈;
                  2、当入队一个线程时,如果队尾是写结点,则直接链接到队尾;
                  3、唤醒线程的规则和AQS类似,都是首先唤醒队首结点。区别是StampedLock中,当唤醒的结点是读结点时,会唤醒该读结点的cowait链中的所有读结点(顺序和入栈顺序相反,也就是后进先出)。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值