[toc]

ReentrantReadWriteLock

内部类Sync简介:

前面看过AQS我们都知道,AQS中使用一个int型变量来表示锁的持有数,但是对于ReentrantReadWriteLock来说,它是分读锁和写锁,那么如何表示这两种锁的持有数呢?读写锁中采用的方式就是将int型一分为二,高16位,所有线程持有的读锁数,低16位用来表示写锁的持有个数;
读锁是一个共享锁,高16位表示的是所有线程总的持读锁数,那如何获取当前线程重入读锁数呢?这里自然引入了ThreadLocal来保存每个线程重入读锁的计数。
        static final int SHARED_SHIFT   = 16;//用于计数出高161位的偏移值(读锁)
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);//65536
        //最大持锁个数
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //用于计数出state的低16位的掩码
        //该方法获取int变量高16位,即读锁持有计数
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        //该方法获取int变量低16位,即写锁持有计数
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
        // HoldCounter变量用于存储每个线程id以及对应的读锁持有计数
        static final class HoldCounter {
            int count = 0;
            // 使用线程id而不是线程引用,是为了防止残留
            // 这里可能是考虑到强引用,垃圾对象无法回收,导致内存泄露
            final long tid = getThreadId(Thread.currentThread());
        }

        // 当前线程持有的读锁计数,ThreadLocalHoldCounter其实就是一个ThreadLocal<HoldCounter>
        private transient ThreadLocalHoldCounter readHolds;
        //下面三个变量是用的比较多的变量,为了提高获取效率,就单独缓存了
        // 保存上个请求读锁线程的HoldCounter。
        private transient HoldCounter cachedHoldCounter;
        // 保存第一个获取到读锁的线程
        private transient Thread firstReader = null;
        // 保存第一个获取到读锁的线程持有的读锁计数
        private transient int firstReaderHoldCount;

读锁

读锁的获取

入口:

    public void lock() {
            sync.acquireShared(1);
    }
    
   public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

tryAcquireShared方法都是交给各种同步器自己定义实现的

        protected final int tryAcquireShared(int unused) {

            Thread current = Thread.currentThread();
            int c = getState();
            //其它线程持有写锁的时候,获取读锁失败
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            // 获取读锁被持有的数量(同一个线程重入也算一次)
            int r = sharedCount(c);
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {// 读锁第一次被获取,需要记录该线程和以及它的持锁记录,这里与后面线程获取读锁时,记录方式不同
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++; // 支持重入
                } else { 
                    // 非第一个获取读锁的线程,会通过readHolds来记录持有读锁树,本质上是ThreadLocal,
                    //里面记录线程持锁数和线程ID,不记录线程对象,是为了GC,防内存泄漏
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        // 此处使用cachedHoldCounter是为了提高效率?减少读出ThreadLocal 的次数?有待思考
                        // 线程第一次获锁时,readHolds.get()中的count计数为0
                        cachedHoldCounter = rh = readHolds.get();
                    // 能走到这里,说明上次缓存的线程和当前线程是同一个
                    //但是上次该线程的缓存锁记录为0,说明它刚释放了读锁,那这里就更新一下内部持锁计数
                    // 个人认为:锁释放的时候存在,虽然改了缓存,还未来得及修改readHolds的情况,这里就提前修改了
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            // 执行下面代码情况:当线程获锁遭阻(队列中有线程正在获取写锁)、读锁持有数量达上限、CAS修改state失败(毕竟并发执行)
            return fullTryAcquireShared(current);
        }

前面分析过,获取读锁失败时,会执行fullTryAcquireShared,分三种情况:

1、由于队列中有线程正在获取写锁导致当前线程获取读锁失败

2、获取读锁时,CAS修改state失败(并发)

3、持读锁数已达上限

fullTryAcquireShared方法针对这三种场景分别处理:

针对场景1:存在其它线程正在获取写锁,那么当前线程获取读锁的时候,该失败还是得失败,但是需要一些处理,防止内存泄漏:如果该线程不是第一个获取读锁的线程,且未能获取到读锁的时候,要清除它的ThreadLocal,因为前面的代码readHolds.get()会自动创建,不清理会内存泄漏,这个好像是为了解决一个jdk的bug,但是第一个去获取读锁的线程,不用管,它没使用ThreadLocal;注意:如果是线程重入读锁,那就不用失败,而是自旋获取读锁。

针对场景2:CAS失败,那就通过自旋,总会成功

针对场景3:读锁数量达到上限,就抛异常。

final int fullTryAcquireShared(Thread current) {

    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        // 如果当前线程持有写锁,在这里又尝试获取读锁,此时队列中若有线程尝试获取写锁,当前线程会被挂起
         // 此时写锁无法释放,会死锁
        } else if (readerShouldBlock()) {
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    // 不是第一个获取读锁线程重入读锁时,不进入该if
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        // 注意readerShouldBlock(),表示走到这里的线程应该阻塞,rh.count == 0表示未获取锁
                        // 这里需要把tryAcquireShared中创建的readHolds清掉,防止内存泄漏
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 走到这里,获锁失败,返回
                // 需要这个判断是因为容许线程重入持有的读锁,保证它往下走
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 这里没啥好说的,结合外面for循环,自旋获取读锁,代码和tryAcquireShared基本一样
        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;
        }
    }
}

当获取锁失败的时候,调用AQS同步器方法:doAcquireShared,作进一步处理,(注:读锁的tryAcquireShared方法不会返回0,但是其它同步工具会,比如信号量)

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED); // 入等待队列
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // 前驱结点为头结点,尝试获取锁
                int r = tryAcquireShared(arg);
                //不同同步器返回值不同,读锁不会返回0
                if (r >= 0) {
                    //设置头结点并唤醒后继结点
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 判断获取锁失败线程是否应该挂起,如果是则挂起
            // 注意内部调用的park方法能响应中断
            // 如果当前线程中断后不清除标志位,线程再也无法被park
             // 这里调用Thread.interrupted()会清掉中断,并在获锁成功后,将中断设置回去。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 发生异常进入,比如tryAcquireShared方法可能会抛异常
        if (failed)
            cancelAcquire(node);
    }
}
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // 保存旧的头结点
    setHead(node);
    // 读锁进入这里propagate肯定大于0,其它同步器,比如信号量,可能等于0,可能走到h.waitStatus < 0这个判断条件,这里需要依赖ws中间态PROPAGATE
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 这里s==null有点不清楚为什么也要走唤醒,是考虑到并发情况,后面又有线程入队了
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

读锁的释放:

入口:

public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
       doReleaseShared();
       return true;
   }
     return false;
}

注意:tryReleaseShared入参无用,也就是释放锁得一步步来。

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // 第一个持锁线程释放锁
    if (firstReader == current) {
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        //非第一个持锁线程释放锁,结合加锁流程看,比较简单
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) { //释放唯一的锁时,清除ThreadLocal,防止内存泄漏
            readHolds.remove();
            if (count <= 0) //未持锁线程释放锁抛异常
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    //自旋修改释放后读锁的数量
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc)) 
           // 锁全部释放完,才去执行doReleaseShared,去唤醒
            return nextc == 0; 
    }
}
上面提到,锁释放完才去唤醒,这是为什么呢?
举例:假设线程A,B 获取了读锁,线程C想要获取写锁,由于有线程持有读锁,线程C只能去同步队列等待,此时线程A,释放锁后,去唤醒C毫无意义,因为B还拿着读锁呢,C不可能成功的,只有等所有线程释放完了,C才能获取写锁,这期间所有要获取读锁的线程,统统排到C后面等待,给C让位,不然的话,C合适才能混出头,拿到写锁啊。(当然,人家持读锁线程,重入读锁不会阻塞喔)

释放锁成功后,自然要去唤醒队列中那些挂起的结点了,了解一下doReleaseShared

private void doReleaseShared() {
    for (;;) {
        Node h = head; // 缓存旧的头结点,后面用于判断头结点是否发生变化
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 复位HEAD的ws状态
                    continue;            // loop to recheck cases
                unparkSuccessor(h);  // 唤醒HEAD的后置结点
            }
            //ws==0 有两种可能:
            //情况1:HEAD结点是新的,后置结点才加入,未来得及修改HEAD的ws,如果CAS不成功,说明后继结点已修改了HEAD的ws,需要重新循环唤醒后继结点
            //情况2:上面代码释放了锁,cas修改了状态,此时应该往下走
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 头结点无变化,退出循环,这里有疑问? 如果唤醒结点获锁失败再次睡眠怎么办?
        // 头结点改变时,唤醒新的头结点的后继结点,会存在多个线程同时去唤醒同一个结点的情况,但是cas保证只有一个成功,
        if (h == head)                   // loop if head changed
            break;
    }
}

上述方法doReleaseShared 中,if(h==head)这个条件检测到HEAD变化,继续唤醒的做法,是为了调高唤醒效率:

比如:队列中HEAD(A)-->B-->C

当A执行doReleaseShared 时,B拿到锁,队列变成:HEAD(B)-->C

当B也执行oReleaseShared释放锁时,如果A还未执行完doReleaseShared ,发现HEAD结点发生了变化,A和B都会去唤醒C,由于CAS,只能有一个成功。

假如B成功,A失败,那么HEAD的ws最终改为PROPAGATE状态。

C被唤醒,C之前是阻塞在doAcquireShared方法中:

   if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;

此时如果C获取锁成功,会执行setHeadAndPropagate(node, r);设置头结点,并唤醒后继,判断逻辑如下:

if (propagate > 0 || h == null || h.waitStatus < 0 ||
    (h = head) == null || h.waitStatus < 0)

其实这里一堆判断逻辑和读锁关系不大了,如果是信号量,就不一样,这里就是想说一下为什么前面要设置PROPAGATE这个状态,其实就是为了满足 h.waitStatus < 0这个条件,唤醒后继结点,这似乎也是为了解决一个JDK bug, 有时间再去研究一下。


SanPiBrother
24 声望3 粉丝

菜鸡的救赎之路