JUC之七:ReentrantReadWriteLock源码解读JDK8

前言

ReentrantReadWriteLock类不仅仅只使用了某一种锁(独占锁或共享锁),而是使用了两种独占锁和共享锁,读的时候允许多个线程同时读,写的时候那其他线程就得乖乖等待了。

1、重要成员

1.1、内部类关系

public class ReentrantReadWriteLock implements ReadWriteLock {
    /** 读锁 */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** 写锁 */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** 持有的AQS子类对象 */
    final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {}

    static final class NonfairSync extends Sync {}

    static final class FairSync extends Sync {}

    public static class ReadLock implements Lock {}

    public static class WriteLock implements Lock {}
}

1.2、构造方法

    public ReentrantReadWriteLock() {
		//m默认非公平锁
        this(false);
    }

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    public static class ReadLock implements Lock {
    	private final Sync sync;
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
    }

    public static class WriteLock implements Lock {
    	private final Sync sync;
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
    }

ReentrantReadWriteLock的构造器默认使用非公平锁。在ReentrantReadWriteLock的构造器中又会去构造ReadLock和WriteLock,从这二者的构造器中可见,它持有的AQS对象是同一个,也就是ReentrantReadWriteLock的AQS成员。重点在于,ReadLock和WriteLock使用的同一个AQS对象,使得可以读写互斥。

1.3、Sync的成员

    abstract static class Sync extends AbstractQueuedSynchronizer {
        static final int SHARED_SHIFT   = 16;
        //2^16
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        //最大共享值,2^16-1
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        //判断独占锁的标志  2^16-1
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** Returns the number of shared holds represented in count  */
        //
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** Returns the number of exclusive holds represented in count  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    }

1.3.1、读锁计数

   static final class HoldCounter {
            int count = 0;
            // Use id, not reference, to avoid garbage retention
            final long tid = getThreadId(Thread.currentThread());
        }

        /**
         * ThreadLocal subclass. Easiest to explicitly define for sake
         * of deserialization mechanics.
         */
        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }
		
        private transient ThreadLocalHoldCounter readHolds;
		//历史上第一个获取读锁的线程
        private transient Thread firstReader = null;
		//历史上第一个获取读锁的线程的计数器,该线程重入的次数
        private transient int firstReaderHoldCount;
		//缓存了线程HoldCounter对象
        private transient HoldCounter cachedHoldCounter;

回想一下Semaphore对共享锁的操作,获取共享锁时Semaphore不会去记录是哪个线程拿到了共享锁,释放共享锁时不管是哪个阿猫阿狗都可以来释放共享锁。

但是read锁不是这样的,他不仅仅会记录线程获得几次锁(包括读锁和写锁记录在state),还会记录每个线程获得了几次读锁

现在需要记录各个线程分别拿走了多少读锁,我们把记录工作交给各个线程自己,通过ThreadLocal让每个线程拥有一个线程私有的HoldCounter对象。如果当前线程没有持有读锁,这个HoldCounter对象为null(因为对ThreadLocal没有使用过get/set);如果当前线程持有着读锁,这个HoldCounter对象不为null,且count成员肯定大于等于1。

cachedHoldCounter / firstReader / firstReaderHoldCount存在的理由,仅仅是为了获得当前线程的HoldCounter对象的一次快速尝试,如果快速尝试失败了,才需要通过ThreadLocal来获得当前线程的HoldCounter对象。

  • 当读锁的计数器为1时,读锁计数器就是通过sharedCount方法计算的,这时firstReader 要么为历史上第一个获取读锁的节点的线程,要么为null(firstReader已经释放完锁了),当firstReader 释放干净读锁后,这时firstReader 还是为null,因为只有释放完所有的读锁后,某线程再次获取读锁,这时firstReader 为该线程。firstReader 作用并不大,只是快速获取读锁信息的以中途径,不然还是可以通过ThreadLocal来获取的
  • cachedHoldCounter一般情况下,这个引用总是指向某个持有读锁的线程的HoldCounter对象。但cachedHoldCounter当好是当前线程的HoldCounter对象这种事情,则完全看缘分(后面会讲到)。

2、写锁的获取和释放

2.1、写锁的获取

WriteLock方法调用的AQS方法是否阻塞是否响应中断是否超时机制返回值及含义
lock()sync.acquire(1)--void
lockInterruptibly()sync.acquireInterruptibly(1)-void
tryLock(long timeout, TimeUnit unit)sync.tryAcquireNanos(1, unit.toNanos(timeout))boolean 返回时是否获得了锁
tryLock()sync.tryWriteLock()-boolean 返回时是否获得了锁

写锁就是独占锁,我们只需要分析tryAcquire的重写即可,后面的阻塞机制就是AQS内部的方法了

    public static class WriteLock implements Lock {
    	private final Sync sync;
    	
        public void lock() {
            sync.acquire(1);
        }
		
		...
    }

从上面的sync.acquire(1)出发,会调用到子类的tryAcquire实现。在此之前,回顾一下tryAcquire返回值的含义,若返回true代表获取独占锁成功,若返回false代表获取独占锁失败。

        protected final boolean tryAcquire(int acquires) {
            //获得当前线程 
            Thread current = Thread.currentThread();
            //获得同步器状态
            int c = getState();
            //获得写锁计数
            int w = exclusiveCount(c);
            //如果c不为0,说明有锁,但不知道是什么锁
            if (c != 0) {
                // 进入分支有两种情况:
                // 1.写锁计数为0。说明此时只有读锁,不能将读锁升级为写锁,所以直接返回false,这儿有个小知识点
                如果同一个线程先获取读锁在获取写锁是不可以的,若先获取写锁再获取读锁是可以的。
                // 2.写锁计数不为0,但不是当前线程持有的写锁。直接返回false。
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                // 加上参数的写锁计数,如果溢出了,就抛出异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 执行到这里,说明肯定是当前线程持有的写锁,那么此时没有线程竞争,
                // 直接set新的写锁计数
                setState(c + acquires);
                return true;
            }
          // 执行到这里,肯定是c == 0,当前既没有读锁,也没有写锁。
          // 但可能有多个线程来竞争这个状态下的任何锁,所以接下来需要通过CAS来竞争
          // writerShouldBlock方法对公平非公平进行了封装,非公平直接返回false,公平锁判断是否存在阻塞的节点
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))// 如果CAS成功,则不会进入此分支
                return false;

			//执行到这里,说明该函数开始检测到 没有任何锁,然后当前线程还获得到了写锁
            setExclusiveOwnerThread(current);
            return true;
        }

具体细节请看注释。我们知道写锁和写锁肯定互斥,写锁也和读锁互斥,所以上面直接返回false的情况挺多的,所以我们不如先说一下返回true的情况(按照程序中的顺序):

  • 之前是由当前线程持有的写锁,所以当前线程现在重入这个写锁。

  • 当前ReentrantReadWriteLock没有任何锁被持有,并且当前线程竞争到了写锁。

直接返回false的情况(按照程序中的顺序):

  • 当前只有读锁,不能将读锁升级为写锁。

  • 当前有写锁,但写锁的持有者不是当前线程。

  • 当前没有任何锁,但判断公平模式后发现当前线程排在了其他线程后面。

  • 当前没有任何锁,判断公平模式后发现可以直接尝试,但CAS竞争失败了。

writerShouldBlock这个函数封装掉了 当前是公平还是非公平 的信息,我们只需要知道该函数返回了false,接下来就可以尝试获得写锁;返回了true,接下来不能去尝试获得写锁,且即将进入阻塞状态(详见AQS#acquire)。
而返回false有两种可能性:

  • 该锁的实现允许插队(即非公平实现)。

  • 当前线程排在了队伍的最前面(即公平实现,但此时同步队列中没有等待的线程)。

接下来看一下AQS子类的新加方法tryWriteLock,非公平的、一次性的获取写锁的方法实现:

        final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {
                int w = exclusiveCount(c);
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

发现它和上面的tryAcquire方法实现几乎一样,除了:

  • 该函数没有参数,只会让写锁计数加1。

  • CAS操作前,没有判断writerShouldBlock。这就是非公平的体现。

  • 判断溢出变得简单,因为只是加1,所以旧值如果刚好等于最大值,那么再加1肯定溢出。

  • 即使是重入写锁(没有线程竞争),也是使用CAS操作增加写锁计数。

2.2、写锁的释放

    public static class WriteLock implements Lock {
    	private final Sync sync;
    	
        public void unlock() {
            sync.release(1);
        }
    }

同样也只是关注tryRelease重写方法即可,之后的过程与AQS独占锁释放一样。

        protected final boolean tryRelease(int releases) {
        	//要释放写锁,首先得保证当前线程已经持有了写锁
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //计算出同步状态的新值
            int nextc = getState() - releases;
            //如果新值的写锁的重入次数为0,那么写锁将被释放
            //free为0说明写锁释放完了,不为0说明该线程写锁重入了
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
            	//如果写锁将完全释放,那么设置ExclusiveOwnerThread成员为null
                setExclusiveOwnerThread(null);
            //不管新值是多少,设置它为state
            setState(nextc);
            return free;
        }

3、读锁的获取和释放

3.1、读锁的获取

ReadLock方法调用的AQS方法是否阻塞是否响应中断是否超时机制返回值及含义
lock()sync.acquireShared(1)--void
lockInterruptibly()sync.acquireSharedInterruptibly(1)-void
tryLock(long timeout, TimeUnit unit)sync.tryAcquireSharedNanos(1, unit.toNanos(timeout))boolean 返回时是否获得了锁
tryLock()sync.tryReadLock()-boolean 返回时是否获得了锁

共享锁的获取前面已经讲解过了,所以接下来我们只需要关心AQS子类对tryAcquireSharedtryReleaseShared的重写实现即可。

    public static class ReadLock implements Lock {
    	private final Sync sync;
    	
        public void lock() {
            sync.acquireShared(1);
        }
        
		...
    }

回顾一下,tryAcquireShared的返回值的情况:

  • 返回>0:说明共享锁获取成功,并且后续线程也可能获取。

  • 返回0:说明共享锁获取成功,后续线程大概率失败。

  • 返回<0:说明共享锁获取失败。

protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
    		1、获取线程共享资源
            int c = getState();
    		2、如果当前写锁被持有&&当前线程不是写锁的线程,直接返回-1,因为读写互斥
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
    		3、获取读锁持有的数量
            int r = sharedCount(c);
    		4、readerShouldBlock判断读锁是否被阻塞,在非公平与公平锁被重写。
             4.1、公平锁就是判断前面是否存在等待的节点
             4.2、非公平锁判断head的后继即第一个阻塞的节点是否是写锁
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&     // 读锁是否小于最大值2^16-1
                compareAndSetState(c, c + SHARED_UNIT)) {  //cas设置state
                5、到这儿说明设置成功,其实已经持有读锁了,这儿是一些善后操作,
                   就算没有这些操作也完全可以运行的
                5.1、读锁为0,说明还没有线程持有读锁,当前线程是第一个获取读锁的
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                5.2、firstReader就是当前线程,说明该线程重入了读锁,直接设置数量+1即可
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                5.3、说明当前线程不是最早获取读锁的线程
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    6、判断当前线程是否是cachedHoldCounter缓存中的,不是则把rh设置成当前线程的持有者
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    6.1、到这儿说明rh肯定被前线程持有了,
                        若count==0说明该线程是第一次获取锁,则设置到ThreadLocal中
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    6.2、线程获取读锁数量+1
                    rh.count++;
                }
                return 1;
            }
    		7、执行到这儿说明第一次获取读锁失败,进入自旋操作尝试获取读锁
            return fullTryAcquireShared(current);
        }
  • 从最开始return -1的地方可知,获取读锁会因为当前写锁不是当前线程所持有而直接返回-1。但获取读锁允许写锁是当前线程所持有而继续尝试获得。

  • 在CAS操作compareAndSetState(c, c + SHARED_UNIT)执行成功后,说明当前线程获取共享锁成功,但还需要做一系列的善后操作。

疑问:为什么firstReader、firstReaderHoldCount、cachedHoldCounter没有采用CAS的方式设置而是直接设置,多线程中不会有问题吗?

只有当state为0时,历史上第一个线程才会去设置firstReader以及firstReaderHoldCount,后续线程无法设置这两个属性,所以不存在线程竞争关系。

cachedHoldCounter在线程中属性会特别的混乱,cachedHoldCounter这个值可以被所有的线程设置,所以说他属于哪个线程会特别的混乱,但若恰好是cachedHoldCounter是当前线程,则不会去threadLocal中去读取了,即使cachedHoldCounter非常混乱,但执行到cachedHoldCounter = rh = readHolds.get();时,已经保证了局部变量rh一定是当前线程的,不需要在关注cachedHoldCounter属于哪个线程,所以也不会存在线程安全的问题,其实就是尝试一次获取,是当前线程就是运气好。

下面进入fullTryAcquireShared自旋进行尝试获取锁:

final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                //第一部分
                int c = getState();
                1、同样判断读写互斥
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                } else if (readerShouldBlock()) {
                    2、进入这儿说明当前线程获取锁可能失败了
                    if (firstReader == current) {
                      2.1、当前线程是重入的,则执行第二部分,设置state,因为此时线程已经获取到锁了
                    } else {
                        3、第一次进入rh肯定为null,这样就可以保证rh是当前线程的
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                3.1、该线程读锁计数为0,说明是初始化的,第一次获取读锁,从ThreadLocal移除掉
                                    若不是0,说明是重入读锁,跳过直接执行第二部分尝试获取锁
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        3.2、对应前面的,直接返回-1,尝试获取锁失败
                        if (rh.count == 0)
                            return -1;
                    }
                }
                //第二部分
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                4、通过cas设置state,成功说明已经获取到锁了,和前面一样做一些善后操作,不在赘述
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

我们把自旋的逻辑分为两个部分:

  • 第一部分负责判断当前线程符不符合继续获得锁的条件,如果不符合则返回-1退出自旋;如果符合,则继续执行第二部分。
  • 第二部分负责CAS修改同步器的状态,如果修改成功,则继续完成善后操作;如果修改失败,继续下一次循环。
  1. if (firstReader == current)分支进入,说明firstReader不为null,从读锁的释放过程来看,只要firstReader不为null,那么firstReaderHoldCount肯定大于0。既然大于0,说明当前线程是在重入读锁,所以给当前线程放行,继续执行第二部分。

  2. if (firstReader == current)的else分支进入,说明当前线程不是firstReader,看来没法通过方便的firstReader来判断,只能依靠其他东西。

    • 如果rh为null,获取到当前线程的HoldCounter对象作为赋值给rh。从整个函数逻辑来看,局部变量rh只要不为null,就肯定是当前线程的HoldCounter对象。重点在于,只要执行到if (rh.count == 0)(指第一条)时,rh就已经是当前线程的ThreadLocal的HoldCounter对象了。

    • 这里需要分两种情况(重入读锁、第一次获取读锁),如果是第一次获取读锁这种情况,那么执行readHolds.get()之前,当前线程是没有HoldCounter对象的(这一点可以从读锁的释放过程得知)。所以readHolds.get()得到的肯定是一个初始的HoldCounter对象,count肯定为0,发现是这种情况,则需要及时清空当前线程的HoldCounter对象(readHolds.remove()),以维持“没有持有读锁时,线程肯定没有ThreadLocal的HoldCounter对象”的规则。接下来第二个if (rh.count == 0)判断会成立就会直接退出循环了。

    • 如果是重入读锁这种情况,那么执行readHolds.get()之前,当前线程是拥有HoldCounter对象的,且count肯定是大于0的。接下来第二个if (rh.count == 0)判断,也不会进入。所以会顺利执行到第二部分。

tryReadLock实现和前面类似,不在解析。

3.2、读锁的释放

同样只关注tryReleaseShared方法即可

    public static class ReadLock implements Lock {
    	private final Sync sync;
    	
        public void unlock() {
            sync.releaseShared(1);
        }
    }

        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            1、如果当前线程是firstReader,修改对应的信息
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                2、反之从ThreadLocal获取读锁信息,减去对应的持有锁数量
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                2.1、当前线程读取锁的次数
                int count = rh.count;
                if (count <= 1) {
                    2.2、count为1说明即将释放完该线程的读锁
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                2.3、count-1
                --rh.count;
            }
            for (;;) {
                3、通过自旋设置state
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    3.1、成功之后,若已经释放完所有的读写锁,返回true,否则返回false
                    这儿和Semaphore信号量有所区别
                    return nextc == 0;
            }
        }
  • 如果firstReader为null,说明历史上第一个reader已经完全释放干净读锁了。反之,无法通过firstReaderHoldCount == 1推导出firstReader不为null。

  • 返回的是nextc == 0,只有在读写锁都是干净的情况,才返回true。这里有点疑问是,在以前讲解的共享锁的释放过程中,是一定要让tryReleaseShared返回true以便接下来调用doReleaseShared来唤醒后面的共享锁节点,难道当前线程释放读锁后,因为别的线程还持有着读锁,所以还是得返回false?

    • 之所以这么做,是因为读锁在获取过程中由于读读不互斥所以基本不会阻塞等待(指当前写锁没有被其他线程持有的情况),而且就算同步队列中有连续的几个共享锁,唤醒后面共享锁节点的任务都在 共享锁获取成功时就做掉了(AQS中setHeadAndPropagate调用了doReleaseShared方法唤醒后续节点)。所以读锁释放成功时,一般不需要返回true。

    • 而返回的true的情况是,释放读锁后,当前读写锁都是干净的,这个时候来唤醒写锁节点才合适。因为写 和读写 都是互斥的。

    • 综上,tryReleaseShared返回true的原因是,为了唤醒写锁节点,在当前读写锁都没被持有的情况下。

4、锁降级

从本文的分析来看,一个线程持有写锁后,可以继续去持有读锁,如果在这之后,这个线程释放了写锁,那么就称写锁现在降级为了读锁。

上面这个过程,细说的话,应该分为两个部分:

  • 一个线程持有写锁后,继续去持有读锁——锁的重入。

  • 同时持有读写锁后,先释放了写锁——锁降级。

在上面fullTryAcquireShared的讲解中,解释了“一个线程持有写锁后,可以继续去持有读锁”的必要性,如果不允许继续去持有读锁,转而进入阻塞等待的过程,会造成死锁的。

如果一个线程持有了读锁,不能继续去持有写锁,从而锁升级。因为可能当前不止有一个线程都持有了读锁,你再去获得写锁是不合理的。

5、总结

  • 同步器的state被划分为两个部分,分别记录被拿走的读锁和写锁的总数。

  • 分别记录各个线程拿走的读锁的工作交给了各个线程自己,通过ThreadLocal实现。

  • 不仅写锁可以重入(这类似于ReentrantLock),读锁也可以重入。

  • 尝试获取写锁时,会因为其他写锁或任意读锁(包括自己)的存在,而进入阻塞等待的过程,抛入sync queue中去。

  • 尝试获取读锁时,会因为其他写锁(不包括自己的写锁)的存在,而进入阻塞等待的过程,抛入sync queue中去。

  • 读锁的非公平获取中,apparentlyFirstQueuedIsExclusive 一定概率防止了写锁无限等待。

  • 锁降级是指,一个线程同时持有读锁和写锁后,先释放了写锁,使得写锁降级为了读锁。

    参考链接:https://blog.csdn.net/anlian523/article/details/106955678

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值