读写锁学习记录

 /**
     * 写锁获取过程
     * @param acquires
     * @return
     */
    protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();
        int c = getState();  //获取线程状态
        int w = exclusiveCount(c);  //获取写锁状态,及c的低16位表示的值
        if (c != 0) {  //c != 0 表示有线程持有锁,可能是读锁也可能是写锁
            //这个判断返回false,目的是过滤掉读锁持有线程及写锁持有线程不是重入写锁的情况(读写锁是互斥的,写写锁是互斥的)
            if (w == 0 || current != getExclusiveOwnerThread())
                return false; //命中了上面的情况
            if (w + exclusiveCount(acquires) > MAX_COUNT)  //写锁用state的低16位表示,大于这个值视为非法
                throw new Error("Maximum lock count exceeded");
            // Reentrant acquire
            setState(c + acquires);  //走到这里的线程必然是写锁,且是写锁重入,所以这个操作不需要原子操作
            return true;  //获取锁成功
        }
        //走到这里表示目前没有线程持有锁,这里会有锁竞争存在,状态的修改需要原子环境
        //writerShouldBlock() 写锁此方法一直返回false
        //此判断过滤掉竞争锁失败的线程
        if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
            return false;
        //设置当前线程为独占线程
        setExclusiveOwnerThread(current);
        return true;
    }

写锁获取重要分两种情况进行

 一  state !=0 主状态不为零,这种情况能够获取写锁的唯一条件是,当前持有锁的线程是写锁,且是写锁重入,其他情况该线程都不可能获得锁

二  state = 0 表示没有任何线程持有锁,当前线程在竞争条件下通过原子操作获取锁,是否成要看运气(不成功则进入阻塞队列)

整个写锁获取围绕上面两个条件进行

 /**
     * 写锁的释放过程
     * @param releases
     * @return
     */
    protected final boolean tryRelease(int releases) {
        //判断当前线程是否是独占线程,如果不是则无法释放任何线程
        //调用释放锁的方法一定是独占线程
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        //走到这里的方法必然是当前独占线程,故不存在并发情况
        int nextc = getState() - releases;
        //判断所有重入写锁是否都释放
        boolean free = exclusiveCount(nextc) == 0;
        if (free) //所有写锁都释放,当前写锁不在持有线程
            setExclusiveOwnerThread(null); //当前独占线程置空
        setState(nextc); //设置读写锁的主状态
        return free;  //true表示所有写线程都释放完毕,反正还有重入锁未释放
    }

写锁的释放很简单,第一步判断进入释放方法的线程是否是当前独占锁(进入写锁释放的方法必然要是当前独占锁)

由于存在锁重入情况,当所有重入锁都释放后,返回成功唤醒阻塞队列排在前面的线程,否则防护false 不唤醒任何阻塞队列中的线程

 /**
     * 读锁的释放过程
     * 1 读锁与读锁之间是共享锁,即一个读锁持有锁,后面的读锁仍然可以获取读锁
     * 2读锁和写锁的关系,读锁和写锁之间是互斥锁,但在获取读锁时,当前线程如果持有写锁,仍然可以获取读锁,当前线程持有读锁则不能获取写锁
     * 该思想贯穿整个读锁获取方法
     * @param unused
     * @return
     */
    protected final int tryAcquireShared(int unused) {

        Thread current = Thread.currentThread();
        int c = getState();  //获取读写锁总体状态
        //写线程持有写锁,且不是持有写锁的线程进行读操作,违反上面介绍的第二条(2)
        // 除了这种情况,其他情况线程都可以竞争获取线程
        if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
            return -1;
        int r = sharedCount(c); //获取当前持有读锁的数量(state的高16位表示的数值)
        //走到这里都是可以竞争锁的线程,(当然执行这个方法的线程都是为了获取读锁,这句话是多余的)
        //readerShouldBlock读锁可以获取线程,阻塞队列head.next不是写锁,r < MAX_COUNT 读锁的数量未达到最大值
        // compareAndSetState(c, c + SHARED_UNIT)当前线程获取读锁,状态变更成功
        if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
            //走到这里表示成获取线程
            //该线程第一次获取读锁,未有重入情况
            //firstReader,firstReaderHoldCount 记录第一次获取读锁的线程,主要作用是简单缓存
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) { //第一次获取读锁的线程重入啦
                firstReaderHoldCount++;         //记录重入次数
            } else {
                //处理非第一次获取读锁的线程
                //这里也是做缓存,优化性能
                // cachedHoldCounter存储最后一次获取读锁的线程
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return 1;
        }
        //上面有
        return fullTryAcquireShared(current);
    }
    /**
     *
     readerShouldBlock() 返回 true,2 种情况:

     在 FairSync 中说的是 hasQueuedPredecessors(),即阻塞队列中有其他元素在等待锁。

     也就是说,公平模式下,有人在排队呢,你新来的不能直接获取锁

     在 NonFairSync 中说的是 apparentlyFirstQueuedIsExclusive(),即判断阻塞队列中 head 的第一个后继节点是否是来获取写锁的,如果是的话,让这个写锁先来,避免写锁饥饿。

     作者给写锁定义了更高的优先级,所以如果碰上获取写锁的线程马上就要获取到锁了,获取读锁的线程不应该和它抢。

     如果 head.next 不是来获取写锁的,那么可以随便抢,因为是非公平模式,大家比比 CAS 速度

     compareAndSetState(c, c + SHARED_UNIT) 这里 CAS 失败,存在竞争。可能是和另一个读锁获取竞争,当然也可能是和另一个写锁获取操作竞争。

     然后就会来到 fullTryAcquireShared 中再次尝试:

     */

    /**
     * 抓住再次获取锁的机会
     * @param current
     * @return
     */
    final int fullTryAcquireShared(Thread current) {

    ReentrantReadWriteLock.Sync.HoldCounter rh = null;
    for (;;) {  //模拟锁自旋操作
        int c = getState();
        //写锁被占用
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            //虽然head.next是写锁,但当前读锁是重入锁,就不用等了啦
        } else if (readerShouldBlock()) {
            //这里表示写锁不持有锁,下面只处理读锁重入的情况
            if (firstReader == current) {
               //读锁重入啦
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        //不是重入线程,count= 0 是上面初始化添加的
                        if (rh.count == 0)
                            readHolds.remove(); //不是读锁重入线程,移除
                    }
                }
                if (rh.count == 0)
                    //去排队
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //非重入情况,竞争获取锁
        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;
        }
    }
    /**
     * 读锁释放过程
     * @param unused
     * @return
     */
    protected final boolean tryReleaseShared(int unused) {
        Thread current = Thread.currentThread();
        if (firstReader == current) {  //释放第一次获取读锁的线程
            // assert firstReaderHoldCount > 0;
            if (firstReaderHoldCount == 1) //第一次获取读锁firstReader 重入释放完成,置为null
                firstReader = null;
            else
                firstReaderHoldCount--;  //自减重入次数
        } else {
            ReentrantReadWriteLock.Sync.HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                rh = readHolds.get(); //缓存中获取当前线程的重入信息
            int count = rh.count;  //当前持有读锁线程重入次数
            if (count <= 1) {   //当前读线程重入释放完成,移除缓存map
                readHolds.remove();
                if (count <= 0)  //非法重入数记录
                    throw unmatchedUnlockException();
            }
            --rh.count; //重入数自减
        }
        for (;;) { //这里模拟线程自旋,保证compareAndSetState(c, nextc)必然会成功
            int c = getState();
            //读锁重入数自减后,变更读写锁state的状态
            int nextc = c - SHARED_UNIT;
            if (compareAndSetState(c, nextc)) //原子操作状态变更
                // Releasing the read lock has no effect on readers,
                // but it may allow waiting writers to proceed if
                // both read and write locks are now free.
                return nextc == 0;
        }
    }

锁降级

Doug Lea 没有说写锁更高级,如果有线程持有读锁,那么写锁获取也需要等待。

不过从源码中也可以看出,确实会给写锁一些特殊照顾,如非公平模式下,为了提高吞吐量,lock 的时候会先 CAS 竞争一下,能成功就代表读锁获取成功了,但是如果发现 head.next 是获取写锁的线程,就不会去做 CAS 操作。

Doug Lea 将持有写锁的线程,去获取读锁的过程称为锁降级(Lock downgrading)。这样,此线程就既持有写锁又持有读锁。

但是,锁升级是不可以的。线程持有读锁的话,在没释放的情况下不能去获取写锁,因为会发生死锁

回去看下写锁获取的源码:

protected final boolean tryAcquire(int acquires) {

    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        // 看下这里返回 false 的情况:
        //   c != 0 && w == 0: 写锁可用,但是有线程持有读锁(也可能是自己持有)
        //   c != 0 && w !=0 && current != getExclusiveOwnerThread(): 其他线程持有写锁
        //   也就是说,只要有读锁或写锁被占用,这次就不能获取到写锁
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        ...
    }
    ...
}

仔细想想,如果线程 a 先获取了读锁,然后获取写锁,那么线程 a 就到阻塞队列休眠了,自己把自己弄休眠了,而且可能之后就没人去唤醒它了


精彩文章 https://www.javadoop.com/post/reentrant-read-write-lock

猜你喜欢

转载自blog.csdn.net/qqchenjunwei/article/details/80374728