并发九:ReentrantReadWriteLock实现分析

ReentrantReadWriteLock

ReentrantReadWriteLock具有ReentrantLock的特性,支持重入和公平性设置,但是对读写进行了分离。
读操作采用共享锁,写操作采用独占锁,即一个资源可以有多个线程同时进行读操作,但是只能有一个线程进行写操作。
在读多写少的情况下ReentrantReadWriteLock可以极大的提高吞吐量。

ReentrantReadWriteLock:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    /** 读锁 */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** 写锁 */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** 同步器实例 */
    final Sync sync;
    /** 父类同步器 */
    abstract static class Sync extends AbstractQueuedSynchronizer {... ...}
    /** 非公平锁同步器 */
    static final class NonfairSync extends Sync {... ...}
    /** 公平锁同步器 */
    static final class FairSync extends Sync {
    /** 构造 */
    public ReentrantReadWriteLock() {
        this(false);
    }
    /** 构造 */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
    /** 工厂方法获取 写锁 */
    public ReentrantReadWriteLock.WriteLock writeLock() { 
        return writerLock; 
    }
    /** 工厂方法获取 读锁 */
    public ReentrantReadWriteLock.ReadLock  readLock()  { 
        return readerLock; 
    }
    ... ...
}

ReentrantReadWriteLock内部实现了三个同步器,与ReentrantLock不同的是,NonfairSync和FairSync的加锁方法都是调用父类Sync的tryAcquire方法,子类中只实现了获取公平策略的方法writerShouldBlock和readerShouldBlock。
Sync:

abstract static class Sync extends AbstractQueuedSynchronizer {
    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;
    /** 共享数量 读锁 高16位*/
    static int sharedCount(int c) {
        return c >>> SHARED_SHIFT;
    }
    /** 独占数量 写锁 低16位*/
    static int exclusiveCount(int c) {
        return c & EXCLUSIVE_MASK;
    }
    /** 重入计数器 */
    static final class HoldCounter {
        int count = 0;
        final long tid = Thread.currentThread().getId();
    }
    /** 重入计数器ThreadLocal */
    static final class ThreadLocalHoldCounter 
                    extends ThreadLocal<HoldCounter> {
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }
    /** 重入计数器ThreadLocal实例 */
    private transient ThreadLocalHoldCounter readHolds;
    /** 最近一个成功获取读锁的线程的计数。
    这省却了ThreadLocal查找 缓存*/
    private transient HoldCounter cachedHoldCounter;
    /** 针对只有一个读锁的优化处理 线程 */
    private transient Thread firstReader = null;
    /** 针对只有一个读锁的优化处理 重入计数器 */
    private transient int firstReaderHoldCount;
    Sync() {
        // 重入计数器容器
        readHolds = new ThreadLocalHoldCounter();
        // 保证readHolds的可见性,因为state 是volatile修饰的
        setState(getState()); 
    }
    // 读公平策略 交由子类实现
    abstract boolean readerShouldBlock();
    // 写公平策略 交由子类实现
    abstract boolean writerShouldBlock();
    ... ...
}
/**非公平锁同步器*/
static final class NonfairSync extends Sync {
    //写公平策略 ,非公平锁中直接返回false
    final boolean writerShouldBlock() {
        return false; 
    }
   //读公平策略 ,判断同步队列中队头等待的节点是否是独占节点,
   //也就判断前面是否有写锁在等待被唤醒。
    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
}
/**公平锁同步器*/
static final class FairSync extends Sync {
    //写公平策略 ,判断队列中是否有等待的节点
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
   //读公平策略 ,判断队列中是否有等待的节点
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

ReadLock和WriteLock使用同一个同步器,这样就需要解决state不够用的问题,因为state既要标示读锁的数量,又要标示写锁的数量,所以将state变量一分为二,高位16位表示读锁的数量,低位16位表示写锁的数量。
读锁存在多个,state的高16位记录锁的数量,重入次数存在每个持有线程的ThreadLocal中,即ThreadLocalHoldCounter。

写锁加锁流程:

        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState(); // s1
            int w = exclusiveCount(c);// 写锁数量
            if (c != 0) { // s2
                // 有读锁存在或者 独占线程非当前线程 
                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;
            } // s3
              // writerShouldBlock 子类实现 主要实现公平策略
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);//持有线程
            return true;
        }

s1:c!=0转入s2,c==转入s3

s2:c!=0说明存在锁
w==0说明写锁的数量为0,那么一定存在读锁,有读锁存在不允许获取写锁因为如果一个线程在读另外一个线程写入,会出现数据不一致引起脏读,返回false。
w不等于0说明存在写锁,只有线程重入才能获取锁,否则返回false,因为写锁是独占锁,不允许两个线程同时写。
如果重入次数大于MAX_COUNT,抛出Error,直接退出程序了。
进入重入逻辑,将state设为c+1,返回true,获取锁成功。

s3:c==0说明不存在锁,首次获取写锁
调用公平性策略方法,如果此时锁为公平锁并且同步队列中有等待的节点,就直接返回false。
如果此时锁为非公平锁,获取成功,返回true。

写锁解锁逻辑与ReentrantLock一样,这里不再累述。

读锁加锁流程:

        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            // 存在写锁(独占锁) 并且请求线程非写锁的持有线程
            // s1
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);// 读锁计数
            // s2
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                // s3
                if (r == 0) {// 只有一个读锁,不动用readHolds
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {//重入
                    firstReaderHoldCount++;
                } else {//多个读锁,启用ThreadLocal记录重入次数 s4
                    HoldCounter rh = cachedHoldCounter;// 访问缓存
                    if (rh == null || rh.tid != current.getId())
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            // 自旋重试 s5
            return fullTryAcquireShared(current);
        }

s1: exclusiveCount(c)!=0 说明存在写锁并且持有该锁的线程不是当前线程,直接返回-1,获取锁失败。因为如果一个线程在写,这时候另外一个线程读取很有可能读取不一致的脏数据。

s2: 调用子类的公平性策略&&r < MAX_COUNT&&CAS设置State,如果三项操作都返回true转入s3,否则转入s5。

s3: r == 0说明没有读锁,是首次获取读锁,直接将firstReader设置为当前线程,重入次数firstReaderHoldCount设为1,获取锁成,返回1。
r!=0且firstReader等于当前线程,说明是唯一的一个线程在重入,直接将firstReaderHoldCount累加,获取锁成,返回1。
r!=0且firstReader不等于当前线程,转入s4。

s4: cachedHoldCounter总是记录最后一次获取锁的线程信息,这样减少了查询ThreadLocal的次数,也提高了后续解锁的效率。
如果cachedHoldCounter为当前线程留下的并且重入次数count为0,先将cachedHoldCounter其放入ThreadLocal中。
如果cachedHoldCounter不是当前线程留下的,从线程的ThreadLocal中获取HoldCounter并将其赋给cachedHoldCounter。
累加cachedHoldCounter中的重入计数器。
获取锁成,返回1。

s5: fullTryAcquireShared逻辑与tryAcquireShared大致相同,使用for(;;)保证compareAndSetState(c, c + SHARED_UNIT)操作成功,因为可能有其他线程在争用,这里自旋等待其他线程操作完毕。

读锁解锁方法:

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) { // s1
    if (firstReaderHoldCount == 1)
        firstReader = null;
    else
        firstReaderHoldCount--;
    } else { // s2
    HoldCounter rh = cachedHoldCounter;
    if (rh == null || rh.tid != current.getId())
        rh = readHolds.get();
    int count = rh.count;
    if (count <= 1) {// 完全释放读锁
        readHolds.remove();
        if (count <= 0)
        throw unmatchedUnlockException();
    }
    --rh.count;// 重入退出
    }
    // s3
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

s1: 如果firstReader(加锁时的第一个线程)不是当前线程转入s3,当前线程没有重入直接firstReader=null,重入则将重入计数器firstReaderHoldCount减1,转入s3。

s2、取出缓存计数器,如果cachedHoldCounter是当前线留下的就从ThreadLock中取出,count <= 1说明这次释放将是完全释放,因此将重入计数器从ThreadLock中删除。重入次数减1,转入s3

s3、state的高位16位操作得到nextc,for(;;)循环中保障state设置成功,如果nextc为0说明读锁全部释放。

锁升级与降级

锁升级是指持有读锁的线程,在读锁未释放的时候再申请写锁
锁降级是指持有写锁的线程,在写锁未释放的时候再申请读锁
一个小栗子:

/** 锁升级 */
public void upgrade() {
    try {
        readLock.lock();// 获取读锁
        try {// 持有读锁
            writeLock.lock();// 再获取写锁
        } finally {
            writeLock.unlock();// 释放写锁
        }
    } finally {
        readLock.unlock();// 最后释放读锁
    }
}
/** 锁降级 */
public void downgrading() {
    try {
        writeLock.lock();// 获取写锁
        try {// 持有写锁
            readLock.lock();// 再获取读锁
        } finally {
            readLock.unlock();// 释放读锁
        }
    } finally {
        writeLock.unlock();// 最后释放写锁
    }
}

通过上面源码的分析:
在"读锁加锁流程s1处"可以看到持有写锁的线程可以再去获取读锁。
在"写锁加锁流程s2处"可以看到有读锁存在是不允许去获取写锁的。

小结

  1. 读锁为共享锁,可以运行多个线程持有同一把读锁;写锁为独占锁,只允许一个线程持有,在读多写少的情况下ReentrantReadWriteLock可以极大的提高吞吐量
  2. 获取读锁的条件:a、没有写锁存在;b、有写锁存在,但是进入读锁的线程是持有写锁的线程。
  3. 获取写锁的条件,a、没有写锁存在(重入除外);b、没有读锁存在。
  4. ReentrantReadWriteLock支持锁降级,不支持锁升级。

码字不易,转载请保留原文连接https://www.jianshu.com/p/2c5c19114463

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

推荐阅读更多精彩内容