当前位置: 代码迷 >> 综合 >> 并发编程(12):ReentrantReadWriteLock 与 StampLock的基本使用与实现原理以及二者的区别。
  详细解决方案

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

热度:61   发布时间:2023-10-24 15:55:41.0

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 modelong 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链中的所有读结点(顺序和入栈顺序相反,也就是后进先出)。