专栏首页用户7113604的专栏ReentranReadWriteLock源码浅析
原创

ReentranReadWriteLock源码浅析

本文为作者个人理解难免有误,欢迎指正

请先阅读ReentranLock源码浅析

ReentranReadWriteLock 的基本构成

  private final ReentrantReadWriteLock.ReadLock readerLock;
    private final ReentrantReadWriteLock.WriteLock writerLock;
    final Sync sync;
    public static class ReadLock
    public static class WriteLock

ReentranReadWriteLock的一个要点

内部的Sync中有如下定义。

c指的AbstractQueuedSynchronizer的state。SHARED_UNIT为65536。

        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 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; }

ReadLock加锁

读锁调用的AbstractQueuedSynchronizer中acquireShared,方法的实现在读锁内部,方法内部一段代码注释帮助大家理解

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

  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 {
                    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);
        }

我们将焦点聚集在tryAcquireShared上。

第一次加锁时state为0,exclusiveCount和sharedCount(c)也是0,经过compareAndSetState(c, c + SHARED_UNIT),c变为65536

如果相同线程再次加读锁,则c的值再次加65536,firstReaderHoldCount变为2

假设此刻c为65536,不同线程尝试加读锁时,sharedCount(c)计算如下为例,将65536表示为32位二进制

00000000000000010000000000000000

无符号右移16位

00000000000000000000000000000001

对应int值1,表示当前读锁加锁数量。此时代码进入

    else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }

创建HoldCounter,并将count设置为1,线程号为当前线程的Id。HoldCounter可以理解为某线程加锁的次数。

如果线程因为并发原因导致无法进入if块,比如在CAS c时失败,就进入fullTryAcquireShared,代码和上述大同小异,主要是进入 for (;;){}循环尝试不停加锁。

如果因为别的线程加了写锁,则因为如下代码,加锁失败。

if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)

纵观try的过程,两个返回值1代表加锁成功,-1代表加锁失败。加锁失败时就会执行doAcquireShared,将当前线程作为一个节点加入双向链表。

方法的具体代码和之前的很相似。区别主要有两点:1.Node的类型为SHARED,2.setHeadAndPropagate替换了setHead,重点在于此。

int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);


private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

满足一系列的判断会调用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))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

doReleaseShared做的事情就是如果当前head节点是SIGNAL,就唤醒后继节点,否则将状态设为PROPAGATE,如果head节点变化了,就结束。

首先如果所有的加锁都是读锁,是不会有一个链表的,必然要么是有线程加了读锁后又有线程尝试加写锁,或者相反。

如下图所示,N0为head,类型为共享锁,所以首先唤醒N0,unpark后循环继续,在判断h==head前,类型为独占的N1加入进来了,但还未更改状态,_所以N1的状态随时有可能变化,从0变成了SIGNAL

或者还没变化,但如果节点一直处于链表中没有唤醒,它的状态会变为SIGNAL_。

unpark前一瞬间的列表

        +------+        +------+ 
        | N0(S)| <----  | N1(E)| 
        |SIGNAL| ---->  |DEFAULT|
        +------+        +------+ 

unpark后一瞬间的列表,此后循环会尝试将DEFAULT值改为PROPAGATE

 +------+ 
 | N1(E) | 
 |DEFAULT|
 +------+ 

此后又有类型为共享的N2加入进来,此刻N1的值可能为PROPAGATE或者SIGNAL

        +---------+        +------+ 
        |  N1(E)  | <----  |  N2(S)| 
        |PROPAGATE| ---->  |DEFAULT|
        +---------+        +------+ 

独占锁在释放时判断只要状态不为0,就可以唤醒后继者,所以N1的状态为PROPAGATE也没有关系。

 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

WriteLock加锁

直接看下已经有线程对加锁的情况下获取写锁的流程。

 if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

如果已经有线程持有读/写锁, c为65536或者其倍数,假设为65536,那么经过exclusiveCount后,即65536&65535,结果为0。

此时getExclusiveOwnerThread()返回null/持有写锁的线程实例,所以加锁失败,那么剩下逻辑就是上篇对AbstractQueuedSynchronizer acquire方法的分析。

如果是同一线程写锁重入,则进行运算1&65535 结果为1,加上已经重入的次数只要不超过最大值就对c+1

if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);

如果加写锁失败,那么流程将进入AbstractQueuedSynchronizer acquireQueued中。相关内容及释放过程已经在前文中描述过了。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • synchronized和ReentrantLock的性能比较

    最近写了个例子,比较了一下synchronized和ReentrantLock的性能,分享一下数据和个人观点。

    小金狗子
  • 一简单线程同步笔试题分享,欢迎纠错分享更多思路

    有线程:worker1、worker2 ,work1只能累加奇数、work2累加偶数,

    小金狗子
  • ReentranLock源码浅析

    常用的lock方法具体实现是在FairSync和NonfairSync。Nonfaire体现在lock时尝试直接获取锁,如果获取失败,则使用和公平锁一样通过Ab...

    小金狗子
  • ThreadPoolExecutor 源码分析

    前面文章的 Thread 我们也分析了,因为 Java 中的Thread 和 内核线程是 1 : 1 的,所以线程是一个重量级的对象,应该避免频繁创建和销毁,我...

    itliusir
  • AQS之同步器

    字面意思就是循环壁垒,使用上与CountDownLatch类似,不过实现上完全不一样,CyclicBarrier统计的的是调用了CyclicBarrier#aw...

    spilledyear
  • 图像凹凸算法

    图像压效果本质的图像坐标的非线性变换,将图像向内挤压,挤压的过程产生压缩变形,从而形成的效果。

    用户3578099
  • 图像凹凸算法(代码全)

    图像压效果本质的图像坐标的非线性变换,将图像向内挤压,挤压的过程产生压缩变形,从而形成的效果。

    用户3578099
  • 面试官虚晃一枪:项目中有用过锁吗?能解释一下什么是AQS?

    锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,如读写锁)。在以前,Jav...

    Java程序猿阿谷
  • 别总追区块链 物联网才是真正的风口

    说起近一段时间内微信圈内最火热的事情,那么就莫过于区块链以及加密货币了,当然在不少人对此趋之若鹜的同时,也有不少人指出这之中充满了炒作与泡沫。实际上,每当一项全...

    人称T客
  • 洛谷P3391 【模板】文艺平衡树(Splay)(FHQ Treap)

    题目背景 这是一道经典的Splay模板题——文艺平衡树。 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区...

    attack

扫码关注云+社区

领取腾讯云代金券