本文概览:在ReetrantReadWriteLock中定义了一个具体同步器Sync,ReetrantReadWriteLock中读锁和写锁的lock和unlock操作都是通过该同步器来实现。

1 读写锁ReadWriteLock介绍

ReentrantLock 实现了线程抢夺同步器的互斥操作,同一个时刻只能有一个线程来使用同步器,在这种情况下任何“读/读”,“写/读”,“写/写”操作都不能同时发生,降低了了吞吐量。为了优化上面的问题引入了读写锁ReadWriteLock来提高吞吐量。

1、ReadWriteLock的使用场景

读写锁使用的场景是一个共享资源被大量读取操作,而只有少量的写操作(增、删、改)。

2、ReadWriteLock的抢夺资源机制如下:

(1)当同步器被读线程占用

  • 同步器能够被多个读线程访问;
  • 只有所有读锁线程释放,才可以获取写锁

(2)当同步器被写线程占用

对于这个线程的读锁可以获取这个同步器,但是对于其他读线程无法获取到同步器。

2 ReadWriteLock的同步器Sync

在ReadWriteLock中定义了一个具体同步器Sync(支持公平和非公平两种机制)。这个同步器实现了共享模式和互斥两个模式(对于ReetrantLock中同步器只实现了排他模式):

  • 实现共享模式,通过实现tryAcquireShared和tryReleasShared的接口。
  • 实现互斥模式,通过实现AQS同步器的排他模式,实现了tryAcquire和tryReelase接口

2.1 共享模式实现

可以通过这个同步器Sync共享模式的acquireShard和releaseShard接口来实现读锁ReadLock:

  • lock,通过同步器Sync的acquireShard实现。
  • unlock,通过同步器Sync的releaseShard实现

2.1.1 tryAcquireShared

对于争夺锁的逻辑tryAcquiredShard的进一步分析

(1)情况1 存在写线程占用同步器

  • 如果为当前读线程和同步器的线程一样,那么此时就可以获取同步器
  • 如果当前读线程和同步器的线程不一样,则不能获取同步器

(2)情况2 只有读线程占用同步器

  • 如果readShouldBlock返回true,如果此时当前读线程的重入次数为0,不能获取。如果重入次数大于0,则可以获取。
  • 如果readShouldBlock返回false,那么此时可以获取到同步器。

(3)情况3 没有线程占用同步器,(此时类似于 互斥模式下下进程,当状态值为0时,此时进行判断shouldBlock)

  • 如果readShouldBlock返回true,不能获取同步器
  • 如果readShouldBlock返回false,能够获取同步器

tryAcquireShard(int acquires)  共享模式下的抢夺资源策略如下:

2.1.1.1 fullTryAcquireShard

对于上面tryAcquireShard流程第二步中readeShouldBlock为true或者CAS为失败的情况,此时提供了两个功能

(1)对于tryAcquireShard中CAS失败的情况处理。

当有多个读线程同时进行获取的情况下,对于ReetrantLock和writeLock,处理CAS失败方法就是将这个线程节点添加到SYNC队列中;对于读锁ReadLock,是可以同时执行的,所以需要有一个while循环重新尝试获取一次。

(2)对于readShouldBlock返回true处理

此时并没有直接进行阻塞这个读线程,而是判断当前线程是否可重入,即进行如下两个判断:

  • 当前读线程可重入次数为0则进行阻塞
  • 这个读线程可重入的次数大于0,说明这个线程已经获取了读锁,那么需要再次获取读锁时,此时就不进行阻塞

注意:和tryAcquire相比,在tryAcquire中只要writerShouldBlock()成功就会阻塞了;而对于tryAcquireShard中readShouldBlock()返回true时还需要fullTryAcquireShard进行上面两个判断。

2.1.1.2 共享模式下的公平性和非公平性机制

通过readerShouldBlock来实现,具体如下:

1、公平性锁

对于公平性的机制是:如果HEAD->NEXT节点包含线程节点,则此时返回true,读线程阻塞

2、非公平性锁

对于非公平性的机制:如果HEAD->NEXT节点为写线程节点,则此时返回true,读线程进行阻塞,其他情况不阻塞。

AQS的apparentlyFirstQueuedIsExclusive()逻辑如下:

3、公平性与非公平性比较

(1)共同点

两种机制都保证:写线程不出现饥饿。

(2)不同点

  • 公平性,最新线程不跟当前线程抢夺资源,直接放置到同步对列后面。
  • 非公平性,最新线程和当前线程抢夺资源

2.1.2 tryReleaseShared

在ReentrantLock中tryRelase就是:(1)重新设置state(2)如果state为0就设置当前进程为null。对于读锁的tryReleaseShared操作也是从这两个方面阐述。

对于ReadLock的tryRelaseShared,不需要设置当前进程了,因为可能同时有多个读线程同时在运行,所以只需要重新设置读的可重入次数就可以了。

1、为什么这里是判断state的值是否为0,而不是判断读锁的次数为0?

tryReleaseShared与tryReleased作用是一样的:都是为了从同步队中获取一个线程来执行。对于tryReleaseShared是为了从同步队列中获取一个写线程。

对于一个读线程而言,即使读的可重入次数不为0,也能由可扩展性来执行同步对列中的读线程,所以tryReleaseShared是为了验证是否可以从同步对列中读取一个写线程,那么就需要判断写锁的次数是否为0,而且为了让写锁能够执行还需要读锁为0,所以此时就需要判断state为0.

2、tryReleaShard和trRelease比较:

(1)读锁执行unlock是为了从同步队列中获取写线程,因为读线程可以通过延展性执行队列中读线程。所以在tryReleaseShared需要判断state是为0,即读和写的可重入次数都为0

(2)写锁执行unlock是为了从同步队列中获取一个线程,这个线程包括读或者写两种。所以在tryRelease时,只需要判断写重入次数为0就可以了

2.2 互斥模式

可以通过同步器的互斥模式acquire和release两个接口来实现写锁WriteLock:

  • lock,通过同步器Sync的acquired实现。
  • unlock,通过同步器Sync的release实现。

2.2.1 tryAcquire 获取同步器

写锁抢夺资源同步器的逻辑为:

(1)存在写线程时,如果是当前线程进行执行,否则阻塞。

(2)在没有线程占用同步器的时候,进行如下判断

  • 如果writeShouldLock为ture时,进行阻塞。
  • 如果writeShouldLock为false时,进行释放。

如下代码:

2.2.1.1 互斥模式下公平性和非公平性

通过writerShouldBlock来实现,具体如下:

1、公平性机制

对于公平性的机制,如果HEAD->NEXT节点包含线程节点,则此时返回true,读写程阻塞

2、非公平性机制

对于非公平机制,不进行任何判断,直接可以准许线程进行抢夺资源同步器

2.2.2 tryRelease

对于WriteLock的tryRelease和ReentrentLock的一样,都需要:(1)重新设置写的可重入次数(2)如果写锁的可重入次数为0就设置当前线程为null。

1、为什么tryRelease判断是写锁的可重入次数为0,不是写锁和读锁的的次数都为0?

这是因为如下情况:当一个写线程中添加了读锁,此时写锁释放,读锁还保存着,此时如果同步队列中第一个线程节点为读线程,那么此时就可以直接运行;但是,如果tryRelease是根据写锁和读锁次数都为0时,才成立,那么此时这个读线程就无法执行了。如下代码

执行结果为

3 ReadLock

3.1 lock

通过同步器Sync的共享模式接口acqurieShard来实现。

3.2 unlock

通过同步器Sync的共享模式接口reaseShard来实现。

4 WriteLock

4.1 Lock

通过同步器互斥模式接口acquire来实现:

4.2 Unlock

通过同步器互斥模式接口releas来实现:

5 读写锁应用实例

对于读锁和写锁简单的应用—读操作使用读锁,写操作使用写锁

6 相关问题

6.1 读锁使用理解

1、以非公平同步器来说明

在使用非公平同步器时,读锁和写锁的区别在阻塞对列为空,且阻塞对列HEAD->NEXT节点不为写节点的时候才能体现出来:

在阻塞对列为空时,如果读线程占用同步器,此时又有读线程过来,则就可以获取同步器。但是如果一旦阻塞对列不为空且HEAD->NEXT节点为写节点,那么此时过来的读线程就要阻塞,就和写锁的tryAcquire的效果一样了 。

发现执行结果就是

这是因为在执行tR2.start进行获取锁时,发现在阻塞对列中发下了线程节点tw了,由于在tryAcquireShared的逻辑就是如果Head->netx节点为写节点,且当前读线程的可重入次数为0,此时就应该阻塞。所以tR2被阻塞。

此时如果将如下代码:

修改代码为如下,此时就可以执行tR2了,因为此时阻塞对列中Head->next没有节点。

所以,此时执行结果为:

2. 对于公平性同步器来言

在使用公平同步器时,读锁和写锁的区别只有在阻塞对列为空的时候才能体现出来:

在阻塞对列为空时,如果读线程占用同步器,此时又有读线程过来,则就可以获取同步器。但是如果一旦阻塞对列不为空,那么此时过来的读线程就要阻塞,就和写锁的tryAcquire的效果一样了 。

6.2 ReetrantReadWriteLock和ReetrantLock的比较

1、比较WriteLock 和 ReentrantLock

两者可以是一样的

2、比较WriteLock和ReadLock

(1) 使用非公平同步器

使用非公平同步器时,读锁和写锁的区别在阻塞对列中的Head->Next节点为读节点的时候或者为空的时候才能体现出来:

在阻塞对列为空时或者Head->next节点不是写线程节点,如果此时读线程占用同步器,那么又有度线程过来,则就可以获取同步器。但是如果一旦阻塞对列的Head->next节点为写节点,那么此时过来的读线程就要阻塞,就和写锁的tryAcquire的效果一样了 。

(2)使用公平同步器

使用公平同步器时,读锁和写锁的区别只有在阻塞对列为空的时候才能体现出来:

在阻塞对列为空时,如果读线程占用同步器,此时又有读线程过来,则就可以获取同步器。但是如果一旦阻塞对列不为空,那么此时过来的读线程就要阻塞,就和写锁的tryAcquire的效果一样了 。

6.3 如何来表示读写锁的同步器是否被占用

1、state

对于ReentrantLock 里,状态值表示重入计数。那么对于读写锁ReetrantReadWriteLock,如何表示读锁和写锁的可重入性和记录相应的可重入的次数?将状态分为高位和低位两部分:

  • 高位表示读锁的所有线程的可重入的数的总和
  • 低位表示写锁的可重入个数。

总结如下:

  • 在ReetrantLock中,我们可以将状态看成获取同步器的标识:如果状态为0时,表示没有ReetrantLock没有获取锁;如果状态大于0时,表示有线程已经占有,状态的值表示的是重入的次数。
  • 在读写ReetrantReadWriteLock锁中,状态的高位和地位都为0,则表示没有任何线程占用,如果高位非0,表示读线程占用,状态值表示的是所有读线程的可重入次数总和;如果低位不为0表示写线程占用,状态值表示重入次数。

可以通过如下两个函数从状态中获取高位和低位的值,sharedCount不为 0 表示分配了读锁,exclusiveCount 不为 0 表示分配了写锁。

2、对于读线程的可重入次数,引入了HoldCounter

在记录读线程的可重入数,有如下几个变量,

查看这些变量的引用位置,发现就是在getReadHoldCount中使用,这个函数作用就是获取当前读线程的可重入次数。

6.4 为什么读锁没有Condition?

首先要明白condition引入的目的:就是为了实现两个互斥的进程顺序的执行。

对于读锁,可能多个读线程同时运行,此时就无法保证进程顺序了,所以就没有condition的用法了。

6.5 为什么读锁在tryAcquireshared时不需要执行 setExclusiveOwnerThread

因为在一个时刻,对于读锁,可能同时又多个线程占用同步器,所以此时就无法进行设置 setExclusiveOwnerThread。

6.6 读锁的”延展性“是否与公平性有关系

当一个wirter1正在执行时,此时依次进入了读线程R1、R2、R3,W2,R4,R5,这三个线程被加入到同步对列中,如果此时writer1执行完了,那么此时就会执行R1,在R1的setHead中运行R2,在R2的setHead中运行R3,直到 W2时停止。

如下代码,设置为公平性和非公平性,执行结果都是一样的

执行结果为:

(全文完)

分类&标签