多线程并发编程9-ReentrantReadWriteLock源码剖析

    前面文章说到了ReentrantLock,解决线程间安全问题,使用ReentrantLock就可以,但是ReentrantLock是独占锁,某一个时刻只能一个线程获取锁,在写少读多的场景下,显然ReentrantLock并不能满足次场景。今天要说的ReentrantReadWriteLock锁就能满足写少读多的场景。

    ReentrantReadWriteLock锁采用读写分离的策略,读锁是一个共享锁,允许多个线程同时获取。写锁是一个独占锁,只允许一个线程获取。当一个线程获取写锁,其他线程不能获取写锁,也不能获取读锁,但是获取写锁的线程可以获取读锁。当一个线程获取读锁,其他线程仍然可以获取读锁,但是无法获取写锁。

    ReentrantReadWriteLock内部维护了一个ReadLock和WriterLock,它们依赖Sync实现具体功能,Sync继承AQS,并且提供了公平和非公平的实现。在AQS内部的表示锁状态的变量只有一个state,哪个ReentrantReadWriteLock是怎么同时表示读锁和写锁的状态呢?ReentrantReadWriteLock将state拆分为高16位低16位,高16位表示读锁的状态,低16位表示写锁的状态。

    下面分别对ReentrantReadWriteLock类读锁、写锁的获取和释放进行源码介绍。

写锁

    ReentrantReadWriteLock内部中的WriterLock是一个独占锁,某时只能有一个线程获取WriterLock。同时WriterLock是一个可重入锁。

获取写锁

    如果当前已经有线程获取到读锁和写锁,则当前请求的线程会被阻塞挂起。如果当前线程已经获取了该锁,再次获取只会简单地把可重入次数加1返回。    

lock() 

public void lock() {

sync.acquire(1);

}

public final void acquire(int arg) {

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

//(1)

    if (c !=0) {

//(2)

        if (w ==0 || current != getExclusiveOwnerThread())

return false;

//(3)

        if (w +exclusiveCount(acquires) >MAX_COUNT)

throw new Error("Maximum lock count exceeded");

        setState(c + acquires);

return true;

    }

//(4)

if (writerShouldBlock() ||

!compareAndSetState(c, c + acquires))

return false;

    setExclusiveOwnerThread(current);

return true;

}

    从源码可以看见ReentrantReadWriteLock的lock方法和ReentrantLock的lock方法内部调用的方法一致,不同的在tryAcquire方法尝试获取资源的实现上。

(1)获取state值并且不为0,说明读锁或写锁已经被某线程获取了。

(2)w=0说明有线程获取到读锁,则返回false。w!=0说明有线程获取到写锁,如果获取到写锁的线程不是当前写锁则返回false。返回false之后,会将当前线程插入到AQS阻塞队列,并阻塞挂起。

(3)执行到这说明当前线程获取到了写锁,则判断重入次数是否超过最大值,没有则增加重入次数,否则抛出error。

(4)代码执行到这说明没有读锁和写锁都没有被线程获取,writerShouldBlock方法判断是否需要阻塞,判断分为非公平和公平,非公平方式则直接返回false,公平方式则判断AQS阻塞队列中是否有非CANCLE状态的节点,有则返回true,否则返回false。writerShouldBlock返回false,则使用CAS算法设置state状态获取锁,设置成功则将当前线程记录到写锁内,否则返回false。返回false之后,会将当前线程插入到AQS阻塞队列,并阻塞挂起。

释放写锁

    尝试释放锁,如果当前线程持有该锁,则将state减一,state为0则当前线程会释放该锁。如果当前线程没有持有该锁则会抛出IllegalMonitorStateException异常。

unlock()

public void unlock() {

sync.release(1);

}

public final boolean release(int arg) {

//(1)

if (tryRelease(arg)) {

Node h =head;

        if (h !=null && h.waitStatus !=0)

unparkSuccessor(h);

return true;

    }

return false;

}

(1)尝试释放写锁,释放成功则从AQS阻塞队列的head节点找到第一个符合条件的node,唤醒node关联的线程。

protected final boolean tryRelease(int releases) {

//(1)

if (!isHeldExclusively())

throw new IllegalMonitorStateException();

//(2)

    int nextc = getState() - releases;

    boolean free =exclusiveCount(nextc) ==0;

    if (free)

setExclusiveOwnerThread(null);

//(3)

    setState(nextc);

    return free;

}

(1)判断当前线程和写锁中记录的线程是否一致,不一致则抛出IllegalMonitorStateException异常。

(2)修改state值,判断state的低16位是否等于0,如果等于0,则将写锁中记录的线程设置为null,即为释放写锁。

(3)设置state值。

读锁

    ReentrantReadWriteLock内部中的ReadLock是一个共享锁,运行多个线程获取ReadLock。同时ReadLock是一个可重入锁,每个获取到ReadLock锁的线程都在线程本地记录重入次数,当重入次数为0的时候就说明该线程释放该ReadLock锁。

获取读锁

    获取读锁,如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS中的state高16位会加1。如果其他线程持有写锁,则当前线程会被阻塞挂起。

lock()

public void lock() {

sync.acquireShared(1);

}

    
public final void acquireShared(int arg) {

if (tryAcquireShared(arg) <0) //(1)

doAcquireShared(arg); //(2)

}

(1)调用ReentrantReadWriteLock中的sync实现的tryAcquireShared方法。

(2)调用AQS的doAcquireShared方法,具体实现点这里

protected final int tryAcquireShared(int unused) {

    Thread current = Thread.currentThread();

    int c = getState();

//(1)

    if (exclusiveCount(c) !=0 &&

getExclusiveOwnerThread() != current)

return -1;

//(2)

    int r =sharedCount(c);

//(3)

    if (!readerShouldBlock() &&

r< MAX_COUNT &&

compareAndSetState(c, c +SHARED_UNIT)) {

 //(4)

if (r ==0) {

firstReader = current;

            firstReaderHoldCount =1;

        }else if (firstReader == current) {     //(5)

firstReaderHoldCount++;

        }else {     //(6)

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;

    }

 //(7)

return fullTryAcquireShared(current);

}

(1)如果写锁有线程获取并且写锁记录的线程并非当前线程则会返回-1,也就反向说明当获取写锁的线程为当前线程,则当前线程可以再获取读锁。注意:当一个线程先获取写锁,后获取读锁,处理完之后要记得把读锁和写锁都释放,不能只释放写锁。

(2)获取读锁计数。

(3)如果判断不需要阻塞并且设置AQS中的state成功,只能有一个线程设置成功,其他失败的线程会执行(7)进行自旋重试。

(4)r=0说明当前线程是第一个尝试获取读锁的线程。

(5)当前线程是第一个获取读锁的线程。

(6)使用cachedHoldCounter记录最后一个获取到读锁的线程和改线程获取读锁的可重入数,readHolds记录了当前线程获取读锁的可重入数。

(7)类似tryAcquireShared,但是是自旋获取。

释放读锁

unlock()

public void unlock() {

sync.releaseShared(1);

}

    
public final boolean releaseShared(int arg) {

//(1)

if (tryReleaseShared(arg)) {

doReleaseShared(); //(2)

return true;

    }

return false;

}

(1)调用ReentrantReadWriteLock中的sync实现的tryReleaseShared方法。

(2)调用AQS的doReleaseShared方法,具体实现点这里

protected final boolean tryReleaseShared(int unused) {

Thread current = Thread.currentThread();

//(1)

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

readHolds.remove();

            if (count <=0)

throw unmatchedUnlockException();

        }

--rh.count;

    }

//(2)

for (;;) {

int c = getState();

        int nextc = c -SHARED_UNIT;

        if (compareAndSetState(c, nextc))

            return nextc ==0;

    }

}

(1)判断当前线程是不是第一个获取读锁的线程,是的话firstReaderHoldCount减1。

(2)通过自旋的方式将state减去一个读计数单位,state减完之后等于0,说明没有当前已经没有线程获取读锁了,则tryReleaseShared返回true。然后会调用doReleaseShared方法释放一个由于获取写锁而阻塞的线程。

    ReentrantReadWriteLock的底层使用AQS,巧妙的将AQS中的state变量分为读写两部分,读共享,写独占,这种在读多写少的场景下比较适用。

     今天的分享就到这,有看不明白的地方一定是我写的不够清楚,所有欢迎提任何问题以及改善方法。

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

推荐阅读更多精彩内容