JUC源码分析-JUC锁(二):ReentrantReadWriteLock

1. 概述

在AQS一篇中我们对“独占锁”和“共享锁”做了简单说明。在J.U.C中,共享锁包括CountDownLatch、CyclicBarrier、Semaphore、ReentrantReadWriteLock、JDK1.8新增的StampedLock等,本篇我们将对ReentrantReadWriteLock做详细分析。

ReentrantReadWriteLock维护了一对相关的锁:共享锁readLock和独占锁writeLock共享锁readLock用于读操作,能同时被多个线程获取;独占锁writeLock用于写入操作,只能被一个线程持有。独占锁的实现和我们上篇所讨论的ReentrantLock相似,共享锁我们接下来会详细分析。ReentrantReadWriteLock具有以下几种特性:

1. 公平性:

  • 非公平模式:默认模式。一个持续争用的非公平锁,可能会使其他读线程或写线程无限延期,但它比公平锁有更高的吞吐量。
  • 公平模式:当构造一个公平锁时,线程争用使用一个近似顺序到达的策略。当目前持有的锁被释放,要么是等待时间最长的单个写入线程被分配写入锁,或者如果有一组读线程比所有等待写线程等待更长的时间,该组将被分配读取锁。

如果已经持有写锁,或者有一个正在等待写的线程,尝试获取公平读锁的线程(非重入)将会阻塞。在等待时间最长的写线程获取并释放写锁之前,当前线程将不能获取读锁。如果一个等待写入的线程放弃等待,并且在队列中等待时间最长的一个或多个读线程正在等待写锁空闲,那么这些读线程将被分配读取锁。

当读锁和写锁都是空闲时(这意味着已经没有等待线程), 一个尝试获取公平写锁(非重入)的线程才会获取成功。注意非阻塞方法tryLock()会立即尝试获取锁,它并不会按照公平原则那样去等待前继节点。

2. 重入性:
ReentrantReadWriteLock允许读线程和写线程重复获取读锁或写锁。当所有写锁都被释放,不可重入读线程才允许获取锁。此外,一个写入线程可以获取读锁,但是一个读线程不能获取写锁。

3. 锁降级:
重入性允许从写锁降级到读锁:首先获取写锁,然后获取读锁,然后释放写锁。不过,从一个读锁升级到写锁是不允许的。读锁和写锁在获取过程中都支持中断。

4. Condition支持:
Condition只有在写锁中用到,读锁是不支持Condition的。

2.数据结构和核心参数

ReentrantReadWriteLock继承关系
  1. ReentrantReadWriteLock实现了ReadWriteLock接口。ReadWriteLock是一个读写锁的接口,提供了"获取读锁的readLock()函数" 和 "获取写锁的writeLock()函数"。
  2. ReentrantReadWriteLock有三个内部类:自定义同步器Sync,读锁ReadLock和写锁WriteLock。和ReentrantLock一样,Sync继承自AQS,也包括了两个内部实现公平锁FairSync和非公平锁NonfairSyncReadLockWriteLock都实现了Lock接口,内部也都分别持有Sync对象。所有的同步功能都是通过Sync来实现的。

Sync源码如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 6317671515068378041L;
    // 最多支持65535(1<<16 -1)个写锁和65535个读锁;低16位表示写锁计数,高16位表示持有读锁的线程数
    static final int SHARED_SHIFT   = 16;
    // 读锁高16位,读锁个数加1,其实是状态值加 2^16
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    // 锁最大数量
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    // 写锁掩码,用于标记低16位
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    //读锁计数,当前持有读锁的线程数,c的高16位
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    //写锁的计数,也就是它的重入次数,c的低16位
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

    //读锁线程数
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    //写锁线程数
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    //当前线程持有的读锁重入数量
    private transient ThreadLocalHoldCounter readHolds;
    //最近一个获取读锁成功的线程计数器
    private transient HoldCounter cachedHoldCounter;
    // 第一个获取读锁的线程
    private transient Thread firstReader = null;
    //firstReader的持有数
    private transient int firstReaderHoldCount;

    // 构造函数
    Sync() {
        readHolds = new ThreadLocalHoldCounter();
        setState(getState()); // ensures visibility of readHolds
    }

    // 持有读锁的线程计数器
    static final class HoldCounter {
        int count = 0; //持有数
        // Use id, not reference, to avoid garbage retention
        final long tid = getThreadId(Thread.currentThread());
    }

    // 本地线程计数器
    static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
        // 重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }
}

Sync类内部存在两个内部类,分别为HoldCounterThreadLocalHoldCounter,用来记录每个线程持有的读锁数量。

3. 源码解析

3.1 ReadLock


public static class ReadLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -5992448646407690164L;
    //持有的AQS对象
    private final Sync sync;

    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }

    //获取共享锁
    public void lock() {
        sync.acquireShared(1);
    }

    //获取共享锁(响应中断)
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    //尝试获取共享锁
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    //释放锁
    public void unlock() {
        sync.releaseShared(1);
    }

    //新建条件
    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }

    public String toString() {
        int r = sync.getReadLockCount();
        return super.toString() +
                "[Read locks = " + r + "]";
    }
}

3.1.1 lock()

public void lock() {
    sync.acquireShared(1);
}

说明:lock方法调用了同步器syncacquireSharedacquireShared在《AQS》篇已经分析过。这里主要介绍一下tryAcquireShared在ReentrantReadWriteLock中的实现。

tryAcquireShared源码如下:

protected final int tryAcquireShared(int unused) {
    //获取当前线程
    Thread current = Thread.currentThread();
    int c = getState();
    //持有写锁的线程可以获取读锁,如果获取锁的线程不是current线程;则返回-1。
    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和firstReaderHoldCount
            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);
}

说明:tryAcquireShared()的作用是尝试获取“读锁/共享锁”。函数流程如下:

  1. 如果“写锁”已经被持有,这时候可以继续获取读锁,但如果持有写锁的线程不是当前线程,直接返回-1(表示获取失败);
  2. 如果在尝试获取锁时不需要阻塞等待(由公平性决定),并且读锁的共享计数小于最大数量MAX_COUNT,则直接通过CAS函数更新读取锁的共享计数,最后将当前线程获取读锁的次数+1。
  3. 如果第二步执行失败,则调用fullTryAcquireShared尝试获取读锁,源码如下:
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {//自旋
        int c = getState();
        //持有写锁的线程可以获取读锁,如果获取锁的线程不是current线程;则返回-1。
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } 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 (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();//当前线程持有读锁数为0,移除计数器
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)//超出最大读锁数量
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {//CAS更新读锁数量
            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;
        }
    }
}

fullTryAcquireShared是获取读锁的完整版本,用于处理CAS失败、阻塞等待和重入读问题。相对于tryAcquireShared来说,执行流程上都差不多,不同的是,它增加了重试机制和对“持有读锁数的延迟读取”的处理。

如果tryAcquireShared获取读锁失败返回-1,则调用doAcquireShared将当前线程加入等待队列尾部等待唤醒,成功获取资源后返回。doAcquireShared方法在《AQS》篇已经分析过,不在赘述。

3.1.2 unlock()

public void unlock() {
    sync.releaseShared(1);
}

说明:unlock方法调用了同步器syncreleaseSharedreleaseShared在《AQS》篇已经分析过。这里主要介绍一下tryReleaseShared在ReentrantReadWriteLock中的实现。

tryReleaseShared源码如下:

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {//当前为第一个获取读锁的线程
        // assert firstReaderHoldCount > 0;
        //更新线程持有数
        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) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {//自旋
        int c = getState();
        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;
    }
}

说明:releaseShared中,首先调用tryReleaseShared尝试释放锁,方法流程很简单,主要包括两步:

  1. 更新当前线程计数器的锁计数;
  2. CAS更新释放锁之后的state,这里使用了自旋,在state争用的时候保证了CAS的成功执行。

注:WriteLock相关的方法可以参考《ReentrantLock》篇的源码分析,实现方式都差不多,这里就不在赘述了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容