一、简介
本文档主要总结JDK中关于ReentrantLock和ReentrantReadWriteLock的相关知识。
首先,介绍AbstractQueuedSynchronizer,在此基础上分析以上两个锁的实现细节。
二、AbstractQueuedSynchronizer
AQS的核心思想是定义了一个同步状态(state)和一组原子操作(如CAS操作)来对状态进行操作。
AQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。
ReentrantLock和ReentrantReadWriteLock都是基于AQS实现的同步器。
- ReentrantLock:ReentrantLock是AQS的一个典型应用,它是一个独占锁(排它锁),同一时刻只能有一个线程持有该锁。ReentrantLock实现了AQS的独占模式,它的同步状态state表示锁的持有状态,通过CAS操作来实现对锁的获取和释放。
- ReentrantReadWriteLock:ReentrantReadWriteLock是AQS的另一个重要应用,它是一个读写锁,允许多个线程同时读取共享资源,但在写操作时只允许一个线程独占。在ReentrantReadWriteLock中,state的高16位表示读锁的数量,低16位表示写锁的数量。
三、ReentrantLock
1.简介
ReentrantLock中的内部类Sync继承自AbstractQueuedSynchronizer。
AQS在其内部实现了一个队列,并且通过其内部属性state的值说明当前锁被获取(acquire)和释放(release)。
ReentrantLock是可重入的互斥锁,当state=0时,表示当前锁没有被持有,当state>=1说明当前锁被持有,state表示锁的重入的次数。
ReentrantLock整体结构组成如下图:
2.非公平锁-加锁方式
- ReentrantLock 的非公平锁策略允许多个线程并发尝试获取锁,即使有其他线程正在等待获取锁。
- 如果第一次 CAS 尝试获取锁失败,当前线程会进入阻塞队列,等待锁的释放,并通过第二次尝试再次获取锁。这样的设计在一定程度上减少了线程切换的开销。
- 非公平锁允许一些后续的线程插队,可能导致前面排队的线程一直无法获取锁,但在某些场景下能够提高并发性能。
2.1 加锁过程图
非公平锁的加锁过程,如图:
其中,尝试获取锁的过程如下图:
2.2 详细代码
加锁过程详细代码如下:
// NonfairSync#lock
final void lock() {
// 首先会尝试马上争抢锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 如果争抢失败
acquire(1);
}
// AQS#acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// NonfairSync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 锁未被占用
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 锁被当前线程占用(重入)
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// AQS#addWaiter
// 将线程节点添加到队列尾部
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果当前链表(队列)为空
enq(node);
return node;
}
// AQS#enq
// 处理当前队列为空的情况
private Node enq(final Node node) {
for (;;) {
// 获取当前节点的尾结点
Node t = tail;
if (t == null) { // Must initialize
// 创建了空的头结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// AQS#acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋 阻塞
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果当前的节点前面还存在其他线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 设置前面的节点的状态为SINGAL(当前面的节点释放锁时,唤醒后面的节点)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
// 前面节点已经被取消,则将其从链表上去除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 阻塞当前线程 并且返回当前线程的中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
3.公平锁-加锁方式
- ReentrantLock 的公平锁策略保证了线程获取锁的顺序按照线程的请求顺序来进行,先到先得。
- 公平锁在尝试获取锁时,如果阻塞队列中已经有节点,会直接进入阻塞队列,而不进行 CAS 尝试直接获取锁。这样的设计保证了线程获取锁的顺序,但也可能增加线程切换的开销。
- 当锁的持有者释放锁后,头部线程会再次进行 CAS 尝试来获取锁,成功后进入临界区执行。这样的设计避免了多个线程同时获取锁,从而保证了公平性。
3.1 加锁过程图
公平锁的加锁过程,如图:
其中,尝试获取锁的过程如下图:
3.2 详细代码
//FairSync#lock
final void lock() {
acquire(1);
}
//AQS#acquire
// 此时arg = 1
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// FairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 如果当前锁不被任何其他线程持有,c = 0
int c = getState();
if (c == 0) {
// 检查当前的队列是否有节点正在排队(与非公平锁的不同,会先检查是否有线程在自己之前申请锁)
if (!hasQueuedPredecessors() &&
// 如果没有节点正在排队,CAS修改当前的state为1
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前的锁被持有,再判断持有锁的线程是不是自己(重入)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
后续同上非公平锁...
4.解锁过程
// ReentrantLock#unlock
public void unlock() {
sync.release(1);
}
// 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;
}
// ReentrantLock#tryRelease
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 判断是否是当前线程的锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 锁的次数为0 释放锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
//AQS#unparkSuccessor
// unpark 当前的node的后继节点
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 正常情况下需要unpark的节点就是当前的节点的后继节点
// 但是如果后继的线程(节点)cancel或者为null,则从后往前遍历
// 找到第一个需要unpark的节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
四、ReentrantReadWriteLock
1.简介
ReentrantReadWriteLock的类关系图:
AQS中维护了一个state状态变量,使用高低位切割实现state状态变量维护两种状态,即高16位表示读状态,低16位表示写状态。
Sync继承AQS实现了如下的核心抽象函数:
- tryAcquire
- release
- tryAcquireShared
- tryReleaseShared
其中,其中tryAcquire、release是为WriteLock写锁准备的;tryAcquireShared、tryReleaseShared是为ReadLock读锁准备的。
写锁可以降级为读锁,防止更新丢失。
Sync中还定义了HoldCounter与ThreadLocalHoldCounter:
- HoldCounter用来记录读锁重入数
- ThreadLocalHoldCounter是ThreadLocal变量,用来存放除第一个获取读锁线程外的其他线程的读锁重入数
NofairSync和FairSync主要实现:非公平模式和公平模式下获取锁时,读写锁是否应该阻塞。
// NofairSync
// 非公平模式下写锁总是阻塞的
final boolean writerShouldBlock() {
return false; // writers can always barge
}
// 非公平模式下 如果等待队列中 下一个节点是写锁 则读锁阻塞
// 为等待的写入操作提供机会,避免写入操作长时间无法执行
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
2.写锁加锁
2.1 函数调用链
加锁过程中,函数调用链如下:
其中,AQS.acquire()是父类提供的模板函数,包含了尝试加锁以及加锁失败的处理流程(创建节点、自旋阻塞重试)。加锁失败的处理流程同上文的ReentrantLock,这里重点关注尝试加锁过程。
2.2 加锁过程图
Sync.tryAcquire()详细过程如下:
2.3 详细代码
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// 不存在写锁(都是读锁) or (存在写锁、加锁的线程不是当前线程)
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;
}
// 当前锁未被任何线程占有 判断是否需要被阻塞再进行CAS加锁操作
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
3.写锁解锁
3.1 函数调用链
解锁流程函数调用链如下:
AQS.release()中除了tryRelease(),其他部分是释放独占锁成功的处理流程。
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 锁释放成功 唤醒队列中的下一个节点
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3.2 解锁过程图
Sync.tryRelease()流程图如下:
3.3 详细代码
protected final boolean tryRelease(int releases) {
// 如果写锁不是当前线程持有
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 写锁数量-1
int nextc = getState() - releases;
// 写锁数量是否为0
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null); // 写锁已经全部释放,设置持有写锁线程为null
// 设置写数量
setState(nextc);
return free;
}
4.读锁加锁
4.1 函数调用链
读锁解锁过程的函数调用链如下:
AQS.acquireShared()中tryacquireShared()是尝试加锁,doAcquireShared()是加锁失败后的处理流程。
// AQS#acquireShared
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
这里先看一下doAcquireShared(),tryAcquireShared()在4.2和4.3详细介绍。
private void doAcquireShared(int arg) {
// 为线程创建共享模式等待节点并将其添加到等待队列的尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
// 自旋,不断尝试获取共享资源
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头节点(表示当前节点是队列中的第一个等待节点)
if (p == head) {
// 尝试获取共享资源
int r = tryAcquireShared(arg);
// 如果获取共享资源成功
if (r >= 0) {
// 设置当前节点为头节点,同时唤醒后继节点,使其尝试获取共享资源
setHeadAndPropagate(node, r);
// help GC,帮助垃圾回收,将当前节点的前驱节点置为null
p.next = null;
// 如果在等待过程中发生过中断,则重新中断当前线程
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 判断是否需要将当前线程阻塞(park),并检查线程是否被中断过
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果获取共享资源失败,则取消当前节点的等待,将其从等待队列中移除
if (failed)
cancelAcquire(node);
}
}
4.2 加锁过程图
Sync.tryAcquireShared()的加锁流程如下图:
4.3 详细代码
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 获取state
int c = getState();
// 如果写锁被其他线程持有,则失败,返回-1
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
// 获取当前读锁数量
int r = sharedCount(c);
// 如果当前线程不应该被阻塞,并且读锁数量未达到饱和,且CAS操作成功
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
// 如果读锁数量为0,则将当前线程设置为第一个读锁线程,并设置读锁计数为1
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 如果当前线程已经是第一个读锁线程,则读锁计数增加1
firstReaderHoldCount++;
} else {
// 如果当前线程不是第一个读锁线程,则从缓存中获取读锁计数器,并将计数器加一
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
// 如果缓存cachedHoldCounter不是当前线程的读锁计数器,则通过readHolds.get()方法从缓存readHolds中获取
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
// 如果之前的计数器count为0,重新放回readHolds缓存中
readHolds.set(rh);
rh.count++;
}
// 返回1表示成功获取读锁
return 1;
}
// 再次尝试获取读锁 与tryAcquireShared()类似,区别是自旋获取
return fullTryAcquireShared(current);
}
5.读锁解锁
5.1 函数调用链
读锁解锁过程的函数调用链如下:
// AQS#releaseShared
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 锁释放成功后唤醒后续节点
doReleaseShared();
return true;
}
return false;
}
下面详细介绍Sync.tryReleaseShared()。
5.2 解锁过程图
Sync.tryReleaseShared()的工作流程图如下:
5.3 详细代码
protected final boolean tryReleaseShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 如果当前线程是第一个获取读锁的线程
if (firstReader == current) {
// 如果第一个获取读锁的线程持有计数器为1,则将其置为null,表示没有读锁被持有
if (firstReaderHoldCount == 1)
firstReader = null;
else
// 否则将计数器减一,表示该线程释放了一个读锁
firstReaderHoldCount--;
} else {
// 如果当前线程不是第一个获取读锁的线程
HoldCounter rh = cachedHoldCounter;
// 检查缓存的读锁计数器是否属于当前线程,如果不是,则从readHolds缓存中获取
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
// 获取当前线程持有的读锁数量
int count = rh.count;
// 如果持有的读锁数量小于等于1
if (count <= 1) {
// 从readHolds缓存中移除当前线程的读锁计数器
readHolds.remove();
// 如果持有的读锁数量小于等于0,表示读锁没有被正确获取过,抛出异常
if (count <= 0)
throw unmatchedUnlockException();
}
// 将计数器减一,表示当前线程释放了一个读锁
--rh.count;
}
// 尝试释放读锁
for (;;) {
// 获取当前状态
int c = getState();
// 计算释放一个读锁后的状态
int nextc = c - SHARED_UNIT;
// 使用CAS操作尝试更新状态为nextc
if (compareAndSetState(c, nextc))
// 释放读锁对读线程没有影响,但它可能会让等待的写线程继续执行
return nextc == 0;
}
}
五、总结
ReentrantLock和ReentrantReadWriteLock都是可重入锁,支持公平与非公平锁,主要区别在于ReentrantLock是独占锁,而ReentrantReadWriteLock是读写锁。
ReentrantLock:
- 特性:
- 独占锁:ReentrantLock是一种独占锁,同一时刻只能有一个线程持有该锁。
- 可重入:同一个线程可以多次获取ReentrantLock而不会被自己阻塞,允许嵌套获取。
- 公平与非公平:ReentrantLock可以通过构造函数指定是否支持公平锁,即等待时间最长的线程优先获取锁。
ReentrantReadWriteLock:
- 特性:
- 读写锁:ReentrantReadWriteLock是一种读写锁,允许多个线程同时读取共享资源,但在写操作时只允许一个线程独占。
- 可重入:与ReentrantLock一样,ReentrantReadWriteLock也是可重入的。
- 公平与非公平:ReentrantReadWriteLock也支持公平和非公平两种获取锁的方式。
在实际应用中,如果读操作远远多于写操作,使用ReentrantReadWriteLock可以提高并发性能。
因为读锁之间是不互斥的,允许多个线程同时读取,而写锁是独占的,只有在没有读取者和写入者时才能获取。
这种读写分离的设计允许多个读取线程并发执行,提高了并发读取的效率,从而提高了整体性能。
但是在写操作较多的情况下,可能会因为写锁争用而导致性能下降。