全网最详细的ReentrantReadWriteLock源码剖析(万字长文)
碎碎念)
花了两天时间,终于把ReentrantReadWriteLock
(读写锁)解析做完了。之前钻研过AQS(AbstractQueuedSynchronizer
)的源码,发现弄懂读写锁也没有想象中那么困难。而且阅读完ReentrantReadWriteLock
的源码,正好可以和AQS的源码串起来理解,相辅相成
AQS的链接贴在下方👇👇👇
全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础
全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(二)资源的获取和释放
全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(三)条件变量
简介
ReentrantReadWriteLock
是一个可重入读写锁,内部提供了读锁和写锁的单独实现。其中读锁用于只读操作,可被多个线程共享;写锁用于写操作,只能互斥访问
ReentrantReadWriteLock
尤其适合读多写少的应用场景
读多写少:
在一些业务场景中,大部分只是读数据,写数据很少,如果这种场景下依然使用独占锁(如synchronized
),会大大降低性能。因为独占锁会使得本该并行执行的读操作,变成了串行执行
ReentrantReadWriteLock
实现了ReadWriteLock
接口,该接口只有两个方法,分别用于返回读锁和写锁,这两个锁都是Lock
对象。该接口源码如下:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock
有两个域,分别存放读锁和写锁:
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
ReentrantReadWriteLock的核心原理主要在于两点:
- 内部类
Sync
:实现了的AQS大部分方法。Sync
类有两个子类FairSync
和NonfairSync
,分别实现了公平读写锁和非公平读写锁。Sync
类及其子类的源码解析会在后面给出 - 内部类
ReadLock
和WriteLock
:分别是读锁和写锁的具体实现,它们都和ReentrantLock
一样实现了Lock
接口,因此实现的手段也和ReentrantLock
一样,都是委托给内部的Sync
类对象来实现,对应的源码解析也会在后面给出
说什么Sync
类、ReadLock
、WriteLock
类啥的都太抽象,不如一张图来得实在!ReentrantReadWriteLock
和这些内部类的继承、聚合关系如下图所示:
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
ReentrantReadWriteLock的特点
读写锁的互斥关系
- 读锁和写锁之间是互斥关系:当有线程持有读锁时,写锁不能获得;当有其他线程持有写锁时,读锁不能获得
- 读锁和读锁之间是共享关系
- 写锁和写锁之间是互斥关系
可重入性
ReentrantReadWriteLock
在ReadWriteLock
接口之上,添加了可重入的特性,且读锁和写锁都支持可重入。可重入的含义是:
- 如果一个线程获取了读锁,那么它可以再次获取读锁(但直接获取写锁会失败,原因见下方的“锁的升降级”)
- 如果一个线程获取了写锁,那么它可以再次获取写锁或读锁
锁的升降级
锁升级
ReentrantReadWriteLock
不支持锁升级,即同一个线程获取读锁后,直接申请写锁是不能获取成功的。测试代码如下:
public class Test1 {
public static void main(String[] args) {
ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.readLock().lock();
System.out.println("get readLock.");
rtLock.writeLock().lock();
System.out.println("blocking");
}
}
运行到第6行会因为获取失败而被阻塞,导致Test1发生死锁。命令行输出如下:
get readLock.
锁降级
ReentrantReadWriteLock
支持锁降级,即同一个线程获取写锁后,直接申请读锁是可以直接成功的。测试代码如下:
public class Test2 {
public static void main(String[] args) {
ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.writeLock().lock();
System.out.println("writeLock");
rtLock.readLock().lock();
System.out.println("get read lock");
}
}
该程序不会产生死锁。结果输出如下:
writeLock
get read lock
Process finished with exit code 0
读写锁的升降级规则总结
ReentrantReadWriteLock
不支持锁升级,因为可能有其他线程同时持有读锁,而读写锁之间是互斥的,因此升级为写锁存在冲突ReentrantReadWriteLock
支持锁降级,因为如果该线程持有写锁时,一定没有其他线程持有读锁或写锁,因此降级为读锁不存在冲突
公平锁和非公平锁
ReentrantReadWriteLock
支持公平模式和非公平模式获取锁。从性能上来看,非公平模式更好
二者的规则如下:
- 公平锁:无论是读线程还是写线程,在申请锁时都会检查是否有其他线程在同步队列中等待。如果有,则让步
- 非公平锁:如果是读线程,在申请锁时会判断是否有写线程在同步队列中等待。如果有,则让步。不过这是为了防止写线程饿死,与公平策略无关;如果是写线程,则直接竞争锁资源,不会关心有无别的线程正在等待
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
Sync类
Sync
类是一个抽象类,有两个具体子类NonfairSync
和FairSync
,分别对应非公平读写锁、公平读写锁。Sync
类的主要作用就是为这两个子类提供绝绝绝大部分的方法实现
只定义了两个抽象方法writerShouldBlock
和readerShouldBlocker
交给两个子类去实现
读状态和写状态
Sync
类利用AQS单个state
字段,来同时表示读状态和写状态,源码如下:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/
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; }
// ······
};
根据上面源码可以看出:
SHARED_SHIFT
表示AQS中的state
(int型,32位)的高16位,作为读状态,低16位作为写状态SHARED_UNIT
二级制为2^16,读锁加1,state
加SHARED_UNIT
MAX_COUNT
就是写或读资源的最大数量,为2^16-1- 使用
sharedCount
方法获取读状态,使用exclusiveCount
方法获取获取写状态
state
划分为读、写状态的示意图(图来自网络)如下,其中读锁持有1个,写锁持有3个:
记录首个获得读锁的线程
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
firstReader
记录首个获得读锁的线程;firstReaderHoldCount
记录firstReader
持有的读锁数
线程局部计数器
Sync
类定义了一个线程局部变量readHolds
,用于保存当前线程重入读锁的次数。如果该线程的读锁数减为0,则将该变量从线程局部域中移除。相关源码如下:
// 内部类,用于记录当前线程重入读锁的次数
static final class HoldCounter {
int count = 0;
// 这里使用线程的id而非直接引用,是为了方便GC
final long tid = getThreadId(Thread.currentThread());
}
/* 内部类,继承ThreadLocal,该类型的变量是每个线程各自保存一份,其中保存的是HoldCounter对象,用set方法保存,get方法获取 */
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;
由于readHolds
变量是线程局部变量(继承ThreadLocal
类),每个线程都会保存一份副本,不同线程调用其get方法返回的HoldCounter对象不同
readHolds
中的HoldCounter
变量保存了每个读线程的重入次数,即其持有的读锁数量。这么做的目的是便于线程释放读锁时进行合法性判断:线程在不持有读锁的情况下释放锁是不合法的,需要抛出IllegalMonitorStateException
异常
缓存
Sync
类定义了一个HoldCounter
变量cachedHoldCounter
,用于保存最近获取到读锁的线程的重入次数。源码如下:
// 这是一个启发式算法
private transient HoldCounter cachedHoldCounter;
设计该变量的目的是:将其作为一个缓存,加快代码执行速度。因为获取、释放读锁的线程往往都是最近获取读锁的那个线程,虽然每个线程的重入次数都会使用readHolds
来保存,但使用readHolds
变量会涉及到ThreadLocal
内部的查找(lookup),这是存在一定开销的。有了cachedHoldCounter
这个缓存后,就不用每次都在ThreadLocal
内部查找,加快了代码执行速度。相当于用空间换时间
获取锁
无论是公平锁还是非公平锁,它们获取锁的逻辑都是相同的,因此Sync
类在这一层就提供了统一的实现
但是,获取写锁和获取读锁的逻辑不相同:
- 写锁是互斥资源,获取写锁的逻辑主要在
tryAcquire
方法 - 读锁是共享资源,获取读锁的逻辑主要在
tryAcquireShared
方法
具体的源码分析见下方的“读锁”和“写锁”各自章节的“获取x锁”部分
释放锁
无论是公平锁还是非公平锁,它们释放锁的逻辑都是相同的,因此Sync
类在这一层就提供了统一的实现
但是,释放写锁和释放读锁的逻辑不相同:
- 写锁是互斥资源,释放写锁的逻辑主要在
tryRelease
方法 - 读锁是共享资源,释放读锁的逻辑主要在
tryReleaseShared
方法
具体的源码分析见下方的“读锁”和“写锁”各自章节的“释放x锁”部分
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
写锁
写锁是由内部类WriteLock
实现的,其实现了Lock
接口,获取锁、释放锁的逻辑都委托给了sync
域(Sync
对象)来执行。WriteLock
的基本结构如下:
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
// 构造方法注入Sync类对象
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// 实现了Lock接口的所有方法
}
获取写锁
WriteLock
使用lock
方法获取写锁,一次获取一个写锁,源码如下:
public void lock() {
sync.acquire(1);
}
lock
方法内部实际调用的是AQS的acquire
方法,源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
而acquire
方法会调用子类Sync
实现的tryAcquire
方法,如下:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 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;
}
分为三步:
1、如果读锁正在被获取中,或者写锁被获取中但不是本线程持有,则获取失败
2、如果获取写锁达到饱和,则抛出错误
3、如果上面两个都不成立,说明此线程可以请求写锁。但需要先根据公平策略来判断是否应该先阻塞。如果不用阻塞,且CAS成功,则获取成功。否则获取失败
其中公平策略判断所调用的writerShouldBlock
,在后面分析公平锁和非公平锁时会给出分析
如果tryAcquire
方法获取写锁成功,则acquire
方法直接返回,否则进入同步队列阻塞等待
tryAcquire
体现的读写锁的特征:
- 互斥关系:
- 写锁和写锁之间是互斥的:如果是别的线程持有写锁,那么直接返回false
- 读锁和写锁之间是互斥的。当有线程持有读锁时,写锁不能获得:如果
c!=0
且w==0
,说明此时有线程持有读锁,直接返回false
- 可重入性:如果当前线程持有写锁,就不用进行公平性判断
writerShouldBlock
,请求锁一定会获取成功 - 不允许锁升级:如果当前线程持有读锁,想要直接申请写锁,此时
c!=0
且w==0
,而exclusiveOwnerThread
是null,不等于current
,直接返回false
释放写锁
WriteLock
使用unlock
方法释放写锁,如下:
public void unlock() {
sync.release(1);
}
unlock
内部实际上调用的是AQS的release
方法,源码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
而该方法会调用子类Sync
实现的tryAcquire
方法,源码如下:
protected final boolean tryRelease(int releases) {
// 如果并不持有锁就释放,会抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 如果释放锁之后锁空闲,那么需要将锁持有者置为null
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free; // 返回锁释放后是否空闲
}
注意:
任何锁的释放都需要判断是否是在持有锁的情况下。如果不持有锁就释放,会抛出异常。对于写锁来说,判断是否持有锁很简单,只需要调用isHeldExclusively
方法进行判断即可;而对于读锁来说,判断是否持有锁比较复杂,需要根据每个线程各自保存的持有读锁数来判断,即readHolds
中保存的变量
尝试获取写锁
WriteLock
使用tryLock
来尝试获取写锁,如下:
public boolean tryLock( ) {
return sync.tryWriteLock();
}
tryLock
内部实际调用的是Sync
类定义并实现的tryWriteLock
方法。该方法是一个final
方法,不允许子类重写。其源码如下:
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1)) // 相比于tryAcquire方法,这里缺少对公平性判断(writerShouldBlock)
return false;
setExclusiveOwnerThread(current);
return true;
}
其实除了缺少对公平策略判断writerShouldBlock
的调用以外,和tryAcquire
方法基本上是一样的,这里不再废话
Lock接口其他方法的实现
// 支持中断响应的lock方法,实际上调用的是AQS的acquireInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
// 实际上调用的是AQS的方法tryAcquireNanos方法
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
// 实际上调用的是Sync类实现的newCondition方法
public Condition newCondition() {
return sync.newCondition();
}
写锁支持创建条件变量,因为写锁是独占锁,而条件变量在await
时会释放掉所有锁资源。写锁能够保证所有的锁资源都是本线程所持有,所以可以放心地去释放所有的锁
而读锁不支持创建条件变量,因为读锁是共享锁,可能会有其他线程持有读锁。如果调用await
,不仅会释放掉本线程持有的读锁,也会释放掉其他线程持有的读锁,这是不被允许的。因此读锁不支持条件变量
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
读锁
读锁是由内部类ReadLock
实现的,其实现了Lock
接口,获取锁、释放锁的逻辑都委托给了Sync
类实例sync
来执行。ReadLock
的基本结构如下:
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
// 构造方法注入Sync类对象
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// 实现了Lock接口的所有方法
}
获取读锁
ReadLock
使用lock
方法获取读锁,一次获取一个读锁。源码如下:
public void lock() {
sync.acquireShared(1);
}
lock
方法内部实际调用的是AQS的acquireShared
方法,源码如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
该方法会调用Sync
类实现的tryAcquireShared
方法,源码如下:
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)) { // CAS失败可能也会导致本能获取成功的线程获取失败
// 如果此时读锁没有被获取,则该线程是第一个获取读锁的线程,记录相应信息
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
}
// 该线程不是首个获取读锁的线程,需要记录到readHolds中
else {
HoldCounter rh = cachedHoldCounter; // 通常当前获取读锁的线程就是最近获取到读锁的线程,所以直接用缓存
// 还是需要判断一下是不是最近获取到读锁的线程。如果不是,则调用get创建一个新的局部HoldCounter变量
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
// 之前最近获取读锁的线程如果释放完了读锁而导致其局部HoldCounter变量被remove了,这里重新获取就重新set
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1; // 如果公平性判断无需让步,且读锁数未饱和,且CAS竞争成功,则说明获取成功
}
return fullTryAcquireShared(current);
}
tryAcquireShared
的返回值说明:
- 负数:获取失败,线程会进入同步队列阻塞等待
- 0:获取成功,但是后续以共享模式获取的线程都不可能获取成功(这里暂时用不上)
- 正数:获取成功,且后续以共享模式获取的线程也可能获取成功
在读写锁中,tryAcquireShared
没有返回0的情况,只会返回正数或负数
前面“Sync
类”中讲解过这些变量,这里再复习一遍:
firstReader
、firstReaderHoldCount
分别用于记录第一个获取到写锁的线程及其持有读锁的数量cachedHoldCounter
用于记录最近获取到写锁的线程持有读锁的数量readHolds
是一个线程局部变量(ThreadLocal
变量),用于保存每个获得读锁的线程各自持有的读锁数量
tryAcquireShared
的流程如下:
1、如果其他线程持有写锁,那么获取失败(返回-1)
2、否则,根据公平策略判断是否应该阻塞。如果不用阻塞且读锁数量未饱和,则CAS请求读锁。如果CAS成功,获取成功(返回1),并记录相关信息
3、如果根据公平策略判断应该阻塞,或者读锁数量饱和,或者CAS竞争失败,那么交给完整版本的获取方法fullTryAcquireShared
去处理
其中上述步骤2如果发生了重入读(当前线程持有读锁的情况下,再次请求读锁),但根据公平策略判断该线程需要阻塞等待,而导致重入读失败。按照正常逻辑,重入读不应该失败。不过,tryAcquireShared
并没有处理这种情况,而是将其放到了fullTryAcquireShared
中进行处理。此外,CAS竞争失败而导致获取读锁失败,也交给fullTryAcquireShared
去处理(fullTryAcquireShared
表示我好难-_-)
fullTryAcquireShared
方法是尝试获取读锁的完全版本,用于处理tryAcquireShared
方法未处理的:
1、CAS竞争失败
2、因公平策略判断应该阻塞而导致的重入读失败
这两种情况。其源码如下:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// 如果当前线程就是firstReader,那么它一定是重入读,不让它失败,而是重新loop直到公平性判断不阻塞为止
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();
}
}
if (rh.count == 0) // 当前线程既不持有读锁(不是重入读),并且被公平性判断为应该阻塞,那么就获取失败
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 下面的逻辑基本上和tryAcquire中差不多,不过这里的CAS如果失败,会重新loop直到成功为止
if (compareAndSetState(c, c + SHARED_UNIT)) {
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
其实和tryAcquire
存在很多的冗余之处,但这么做的目的主要是让tryAcquireShared
变得更简单,不用处理复杂的CAS循环
fullTryAcquireShared
主要是为了处理CAS失败和readerShouldBlock
判true而导致的重入读失败,这两种情况在理论上都应该成功获取锁。fullTryAcquireShared
的做法就是将这两种情况放在for
循环中,一旦发生就重新循环,直到成功为止
tryAcquireShared
和fullTryAcquireShared
体现的读写锁特征:
- 互斥关系:
- 读锁和读锁之间是共享的:即使有其他线程持有了读锁,当前线程也能获取读锁
- 读锁和写锁之间是互斥的。当有其他线程持有写锁,读锁不能获得:
tryAcquireShared
第4-6行,fullTryAcquireShared
第5-7行都能体现这一特征
- 可重入性:如果当前线程获取了读锁,那么它再次申请读锁一定能成功。这部分逻辑是由
fullTryAcquireShared
的for
循环实现的 - 支持锁降级:如果当前线程持有写锁,那么它申请读锁一定会成功。这部分逻辑见
tryAcquireShared
第5行,current
和exclusiveOwnerThread
是相等的,不会返回-1
释放读锁
ReadLock
使用unlock
方法释放读锁,如下:
public void unlock() {
sync.releaseShared(1);
}
unlock
方法实际调用的是AQS的releaseShared
方法,如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
而该方法会调用Sync
类实现的tryReleaseShared
方法,源码如下:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
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(); // 如果释放读锁后不再持有锁,那么移除readHolds保存的线程局部HoldCounter变量
if (count <= 0)
throw unmatchedUnlockException(); // 抛出IllegalMonitorStateException异常
}
--rh.count;
}
// 循环CAS保证修改state成功
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0; // 如果释放后锁空闲,那么返回true,否则返回false
}
}
如果返回true,说明锁是空闲的,releaseShared
方法会进一步调用doReleaseShared
方法,doReleaseShared
方法会唤醒后继线程并确保传播(确保传播:保证被唤醒的线程可以执行唤醒其后续线程的逻辑)
尝试释放读锁
ReadLock
使用tryLock
方法尝试释放读锁,源码如下:
public boolean tryLock() {
return sync.tryReadLock();
}
tryLock
内部实际调用的是Sync
类定义并实现的tryReadLock
方法。该方法是一个final
方法,不允许子类重写。其源码如下:
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (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 true;
}
}
}
其实除了缺少对公平策略判断方法readerShouldBlock
的调用以外,和tryAcquireShared
方法基本上是一样的
Lock接口其他方法的实现
// 支持中断响应的lock方法,实际上调用的是AQS的acquireSharedInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 实际上调用的是AQS的方法tryAcquireSharedNanos方法
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// 读锁不支持创建条件变量
public Condition newCondition() {
throw new UnsupportedOperationException();
}
和写锁的区别在于,读锁不支持创建条件变量。如果调用newCondition
方法,会直接抛出UnsupportedOperationException
异常。不支持的原因在前面已经分析过,这里不再赘述
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
读写锁的公平策略
ReentrantReadWriteLock
默认构造方法如下:
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
说明其默认创建的是非公平读写锁。如果要创建公平读写锁,需要使用有参构造函数,参数fair设置为true
公平读写锁
公平读写锁依赖于Sync
的子类FairSync
来实现,其源码如下:
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
writerShouldBlock
writerShouldBlock
实际上调用的是AQS的hasQueuedPredecessors
方法,该方法会检查是否有线程在同步队列中等待,源码如下:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 如果head等于tail,说明是空队列
// 如果队首的thread域不是当前线程,说明有别的线程先于当前线程等待获取锁
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
writerShouldBlock
只有在tryAcquire
中被调用。如果当前线程请求写锁时发现已经有线程(读线程or写线程)在同步队列中等待,则让步
readerShouldBlock
readerShouldBlock
和writerShouldBlock
一样,都是调用AQS的hasQueuedPredecessors
方法
readerShouldBlock
只有在tryAcquireShared
(fullTryAcquireShared
)中被调用。如果当前线程请求读锁时发现已经有线程(读线程or写线程)在同步队列中等待,则让步
非公平读写锁
非公平读写锁依赖于Sync
的子类NonfairSync
来实现,其源码如下:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
writerShouldBlock
writerShouldBlock
直接返回false
writerShouldBlock
只有在tryAcquire
中被调用,返回false表示在非公平模式下,不管是否有线程在同步队列中等待,请求写锁都不会让步,而是直接上去竞争
readerShouldBlock
readerShouldBlock
实际调用的是AQS的apparentlyFirstQueuedIsExclusive
方法。其源码如下:
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
如果同步队列为空,或队首线程是读线程(获取读锁而被阻塞),则返回false。如果同步队列队首线程是写线程(获取写锁而被阻塞),则返回true
readerShouldBlock
只有在tryAcquireShared
(fullTryAcquireShared
)中被调用。如果当前线程请求读锁时发现同步队列队首线程是写线程,则让步。如果是读线程则跟它争夺锁资源
这么做的目的是为了防止写线程被“饿死”。因为如果一直有读线程前来请求锁,且读锁是有求必应,就会使得在同步队列中的写线程一直不能被唤醒。不过,apparentlyFirstQueuedIsExclusive
只是一种启发式算法,并不能保证写线程一定不会被饿死。因为写线程有可能不在同步队列队首,而是排在其他读线程后面
读写锁的公平策略总结
公平模式:
无论当前线程请求写锁还是读锁,只要发现此时还有别的线程在同步队列中等待(写锁or读锁),都一律选择让步
非公平模式:
- 请求写锁时,当前线程会选择直接竞争,不会做丝毫的让步
- 请求读锁时,如果发现同步队列队首线程在等待获取写锁,则会让步。不过这是一种启发式算法,因为写线程可能排在其他读线程后面
如果觉得作者写的还可以的话,可以👍鼓励一下
出处:https://www.cnblogs.com/frankiedyz/p/15655865.html
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任