Java多线程与高并发专题——深入ReentrantReadWriteLock
深入ReentrantReadWriteLock
读写锁出现原因
synchronized和ReentrantLock都是互斥锁。如果说有一个操作是读多写少的,还要保证线程安全的话。如果采用上述的两种互斥锁,效率方面很定是很低的。在这种情况下,咱们就可以使用ReentrantReadWriteLock读写锁去实现。读读之间是不互斥的,可以读和读操作并发执行。但是如果涉及到了写操作,那么还得是互斥的操作。
总结起来,主要是以下原因:
1. 读多写少场景下的性能优化
问题:在传统的独占锁机制下,任何线程(无论是读操作还是写操作)在访问共享资源时,都需要获取到锁。这意味着在同一时间只能有一个线程(读或写)能够访问资源,导致大量读线程阻塞等待,降低了系统的吞吐量。
解决方案:读写锁允许多个读线程同时读取共享资源,而写线程则需要独占锁。这样可以在读操作占绝大多数的场景下,大大提高并发性能。例如,在一个数据库查询系统中,读取数据的频率远高于写入数据的频率,使用读写锁可以让多个读线程同时访问数据,而写线程在需要时可以独占资源进行更新。
2. 提供更细粒度的锁控制
灵活性:读写锁为开发者提供了更灵活的锁控制方式。开发者可以根据不同的业务场景选择合适的锁模式。例如,在读操作需要频繁执行且对数据一致性要求不高的情况下,可以使用读锁;在需要修改数据且对数据一致性严格要求的情况下,可以使用写锁。
可升级锁:读写锁还支持锁的升级机制。一个线程可以在持有读锁的情况下,升级为写锁,从而进行写操作。这使得线程可以在不释放锁的情况下,从读模式切换到写模式,避免了频繁获取和释放锁的开销。
3. 减少锁竞争
降低锁等待时间:由于读写锁允许多个读线程同时访问共享资源,减少了读线程之间的锁竞争。这使得读线程可以更快地获取到锁,减少线程的等待时间,提高了系统的响应速度。
提高CPU利用率:在读多写少的场景下,读写锁可以充分利用多核CPU的处理能力。多个读线程可以被分配到不同的CPU核心上并行执行,从而提高CPU的利用率。
4. 支持更复杂的并发控制语义
条件变量:读写锁可以与条件变量结合使用,实现更复杂的并发控制语义。例如,在读写锁的保护下,线程可以等待特定的条件满足后再进行读或写操作。
锁转换:读写锁支持读锁和写锁之间的转换。这使得线程可以在不同的锁模式之间灵活切换,满足不同的业务需求。
使用示例
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
// 创建一个读写锁实例
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 获取读写锁的写锁对象
static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
// 获取读写锁的读锁对象
static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
public static void main(String[] args) throws InterruptedException {
// 启动一个子线程,获取读锁
new Thread(() -> {
readLock.lock(); // 获取读锁
try {
System.out.println("子线程获取读锁!");
try {
// 子线程休眠500秒,模拟读取数据的过程
Thread.sleep(500000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
// 确保在任何情况下都释放读锁
readLock.unlock();
System.out.println("子线程释放读锁!");
}
}).start();
// 主线程休眠1秒,等待子线程启动
Thread.sleep(1000);
// 主线程试图获取写锁
writeLock.lock(); // 获取写锁
try {
System.out.println("主线程获取写锁!");
// 在这里可以进行写操作,但由于子线程持有读锁,主线程会阻塞
} finally {
// 确保在任何情况下都释放写锁
writeLock.unlock();
System.out.println("主线程释放写锁!");
}
}
}
实现原理
ReentrantReadWriteLock还是基于AQS实现的,还是对state进行操作,拿到锁资源就去干活,如果没有拿到,依然去AQS队列中排队。
- 读锁操作:基于state的高16位进行操作。
- 写锁操作:基于state的低16为进行操作。
ReentrantReadWriteLock依然是可重入锁。
- 写锁重入:读写锁中的写锁的重入方式,基本和ReentrantLock一致,没有什么区别,依然是对state进行+1操作即可,只要确认持有锁资源的线程,是当前写锁线程即可。只不过之前ReentrantLock的重入次数是state的正数取值范围,但是读写锁中写锁范围就变小了。
- 读锁重入:因为读锁是共享锁。读锁在获取锁资源操作时,是要对state的高16位进行 + 1操作。因为读锁是共享锁,所以同一时间会有多个读线程持有读锁资源。这样一来,多个读操作在持有读锁时,无法确认每个线程读锁重入的次数。为了去记录读锁重入的次数,每个读操作的线程,都会有一个ThreadLocal记录锁重入的次数。
写锁的饥饿问题
读锁是共享锁,当有线程持有读锁资源时,再来一个线程想要获取读锁,直接对state修改即可。在读锁资源先被占用后,来了一个写锁资源,此时,大量的需要获取读锁的线程来请求锁资源,如果可以绕过写锁,直接拿资源,会造成写锁长时间无法获取到写锁资源。
读锁在拿到锁资源后,如果再有读线程需要获取读锁资源,需要去AQS队列排队。如果队列的前面需要写锁资源的线程,那么后续读线程是无法拿到锁资源的。持有读锁的线程,只会让写锁线程之前的读线程拿到锁资源
源码注释
- Acquisition order
This class does not impose a reader or writer preference ordering for lock access. However, it does support an optional fairness policy.
Non-fair mode (default)
When constructed as non-fair (the default), the order of entry to the read and write lock is unspecified, subject to reentrancy constraints. A nonfair lock that is continuously contended may indefinitely postpone one or more reader or writer threads, but will normally have higher throughput than a fair lock.
Fair mode
When constructed as fair, threads contend for entry using an approximately arrival-order policy. When the currently held lock is released, either the longest-waiting single writer thread will be assigned the write lock, or if there is a group of reader threads waiting longer than all waiting writer threads, that group will be assigned the read lock.
A thread that tries to acquire a fair read lock (non-reentrantly) will block if either the write lock is held, or there is a waiting writer thread. The thread will not acquire the read lock until after the oldest currently waiting writer thread has acquired and released the write lock. Of course, if a waiting writer abandons its wait, leaving one or more reader threads as the longest waiters in the queue with the write lock free, then those readers will be assigned the read lock.
A thread that tries to acquire a fair write lock (non-reentrantly) will block unless both the read lock and write lock are free (which implies there are no waiting threads). (Note that the non-blocking ReentrantReadWriteLock. ReadLock. tryLock() and ReentrantReadWriteLock. WriteLock. tryLock() methods do not honor this fair setting and will immediately acquire the lock if it is possible, regardless of waiting threads.) - Reentrancy
This lock allows both readers and writers to reacquire read or write locks in the style of a ReentrantLock. Non-reentrant readers are not allowed until all write locks held by the writing thread have been released.
Additionally, a writer can acquire the read lock, but not vice-versa. Among other applications, reentrancy can be useful when write locks are held during calls or callbacks to methods that perform reads under read locks. If a reader tries to acquire the write lock it will never succeed. - Lock downgrading
Reentrancy also allows downgrading from the write lock to a read lock, by acquiring the write lock, then the read lock and then releasing the write lock. However, upgrading from a read lock to the write lock is not possible. - Interruption of lock acquisition
The read lock and write lock both support interruption during lock acquisition. - Condition support
The write lock provides a Condition implementation that behaves in the same way, with respect to the write lock, as the Condition implementation provided by ReentrantLock. newCondition does for ReentrantLock. This Condition can, of course, only be used with the write lock.
The read lock does not support a Condition and readLock().newCondition() throws UnsupportedOperationException. - Instrumentation
This class supports methods to determine whether locks are held or contended. These methods are designed for monitoring system state, not for synchronization control.
Serialization of this class behaves in the same way as built-in locks: a deserialized lock is in the unlocked state, regardless of its state when serialized.
Sample usages. Here is a code sketch showing how to perform lock downgrading after updating a cache (exception handling is particularly tricky when handling multiple locks in a non-nested fashion):
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
ReentrantReadWriteLocks can be used to improve concurrency in some uses of some kinds of Collections. This is typically worthwhile only when the collections are expected to be large, accessed by more reader threads than writer threads, and entail operations with overhead that outweighs synchronization overhead. For example, here is a class using a TreeMap that is expected to be large and concurrently accessed.
class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try {
return m.get(key);
} finally {
r.unlock();
}
}
public String[] allKeys() {
r.lock();
try {
return m.keySet().toArray();
} finally {
r.unlock();
}
}
public Data put(String key, Data value) {
w.lock();
try {
return m.put(key, value);
} finally {
w.unlock();
}
}
public void clear() {
w.lock();
try {
m.clear();
} finally {
w.unlock();
}
}
}
翻译:
以下是一个支持与ReentrantLock
类似语义的ReadWriteLock
的实现。该类具有以下特性:
获取顺序
此类并未规定读锁或写锁的优先获取顺序。然而,它支持一种可选的公平性策略。
-
非公平模式(默认):当以非公平模式构建(默认模式)时,读锁和写锁的进入顺序是不指定的,需遵循可重入约束。一个持续被竞争的非公平锁可能会无限期地推迟一个或多个读线程或写线程,但通常比公平锁具有更高的吞吐量。
-
公平模式:当以公平模式构建时,线程按照大致的到达顺序竞争进入。当当前持有的锁被释放时,要么是等待时间最长的单个写线程将被分配写锁,要么是等待时间比所有等待写线程都长的一组读线程将被分配读锁。尝试获取公平读锁(不可重入地)的线程将在写锁被持有或存在等待的写线程时阻塞。该线程在最老的等待写线程已经获取并释放写锁之后才会获取读锁。当然,如果等待的写线程放弃等待,留下一个或多个读线程作为队列中最长的等待者且写锁空闲,则这些读线程将被分配读锁。尝试获取公平写锁(不可重入地)的线程将阻塞,除非读锁和写锁都空闲(这意味着没有等待的线程)。(注意,非阻塞的
ReentrantReadWriteLock.ReadLock.tryLock()
和ReentrantReadWriteLock.WriteLock.tryLock()
方法不会遵守公平设置,如果可能的话,会立即获取锁,无论等待的线程如何。)
可重入性
此锁允许读线程和写线程以ReentrantLock
的风格重新获取读锁或写锁。在写线程持有的所有写锁被释放之前,不允许不可重入的读锁。此外,写线程可以获取读锁,但反之则不行。在写锁被持有期间,如果调用或回调的方法在读锁下执行读操作,可重入性将非常有用。如果读线程尝试获取写锁,它将永远不会成功。
锁降级
可重入性还允许从写锁降级到读锁,通过获取写锁,然后获取读锁,最后释放写锁来实现。然而,从读锁升级到写锁是不可能的。
锁获取的中断
读锁和写锁都支持在锁获取过程中进行中断。
条件支持
写锁提供了一个与ReentrantLock.newCondition
提供的条件实现方式相同的Condition
实现。这个Condition
当然只能与写锁一起使用。读锁不支持Condition
,readLock().newCondition()
将抛出UnsupportedOperationException
。
监控
此类支持方法来确定锁是否被持有或竞争。这些方法旨在用于监控系统状态,而不是用于同步控制。此类的序列化行为与内置锁相同:反序列化的锁处于未锁定状态,无论其在序列化时的状态如何。
示例用法
以下是一个代码示例,展示了如何在更新缓存后执行锁降级(在非嵌套方式处理多个锁时,异常处理尤其棘手):
class CachedData {
// 缓存数据
Object data;
// 表示缓存是否有效的标志
volatile boolean cacheValid;
// 读写锁
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
// 获取读锁
rwl.readLock().lock();
try {
// 检查缓存是否有效
if (!cacheValid) {
// 必须释放读锁,才能获取写锁
rwl.readLock().unlock();
try {
// 获取写锁
rwl.writeLock().lock();
try {
// 重新检查状态,因为另一个线程可能已经获取了写锁并改变了状态
if (!cacheValid) {
// 更新数据
data = ...; // 从数据源加载数据
// 标记缓存有效
cacheValid = true;
}
// 降级:在释放写锁之前获取读锁
rwl.readLock().lock();
} finally {
// 释放写锁,仍然持有读锁
rwl.writeLock().unlock();
}
} catch (Exception e) {
// 异常处理
System.err.println("Error acquiring lock: " + e.getMessage());
// 重新锁获取逻辑可能会更复杂,这里仅作为示例
}
}
// 使用缓存数据
use(data);
} finally {
// 释放读锁
rwl.readLock().unlock();
}
}
}
可重入读写锁(ReentrantReadWriteLocks)可以在某些集合类的某些使用场景中提高并发性能。这通常只有在集合预计很大、被更多的读线程访问而不是写线程、并且涉及的操作开销大于同步开销时才值得。例如,以下是一个使用 TreeMap 的类,预计该 TreeMap 会很大并且会被并发访问。
class RWDictionary {
// 使用TreeMap作为底层数据结构
private final Map<String, Data> m = new TreeMap<>();
// 读写锁
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
private final Lock r = rwl.readLock();
// 写锁
private final Lock w = rwl.writeLock();
// 获取键对应的值
public Data get(String key) {
// 获取读锁
r.lock();
try {
// 返回对应键的值
return m.get(key);
} finally {
// 释放读锁
r.unlock();
}
}
// 获取所有键
public String[] allKeys() {
// 获取读锁
r.lock();
try {
// 将键集合转换为数组
return m.keySet().toArray(new String[0]);
} finally {
// 释放读锁
r.unlock();
}
}
// 插入键值对
public Data put(String key, Data value) {
// 获取写锁
w.lock();
try {
// 插入键值对,并返回旧值
return m.put(key, value);
} finally {
// 释放写锁
w.unlock();
}
}
// 清空字典
public void clear() {
// 获取写锁
w.lock();
try {
// 清空字典
m.clear();
} finally {
// 释放写锁
w.unlock();
}
}
}
写锁加锁流程
/**
* 尝试获取写锁。
*
* <p>如果读锁和写锁都没有被其他线程持有,则获取写锁,并立即返回,同时将写锁持有计数设置为 1。
*
* <p>如果当前线程已经持有写锁,则持有计数加 1,并立即返回。
*
* <p>如果锁被其他线程持有,则当前线程将被禁用线程调度,进入休眠状态,直到获取到写锁,此时写锁持有计数将被设置为 1。
*/
public void lock() {
sync.acquire(1);
}
==>
/**
* 以独占模式获取锁,忽略中断。
* 通过至少调用一次 {@link #tryAcquire} 方法来实现,若成功则返回。
* 否则,线程将被放入队列中,可能会反复阻塞和解除阻塞,不断调用 {@link #tryAcquire} 直到成功。
* 此方法可用于实现 {@link Lock#lock} 方法。
*
* @param arg 用于获取锁的参数。该值会传递给 {@link #tryAcquire} 方法,但除此之外没有其他特殊含义,可以表示任意内容。
*/
public final void acquire(int arg) {
// 尝试获取锁,如果获取失败并且将当前线程加入等待队列后被中断
if (!tryAcquire(arg) &&
// 先将当前线程封装成一个独占模式的节点添加到等待队列中,然后尝试在队列中获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 重新设置当前线程的中断状态
selfInterrupt();
}
==>
/**
* 尝试以独占模式获取锁。
*
* 此方法的逻辑如下:
* 1. 如果读锁计数不为零或者写锁计数不为零且当前线程不是锁的持有者,则获取失败。
* 2. 如果锁计数将达到饱和(即超过最大计数),则获取失败。此情况仅在计数已经不为零时可能发生。
* 3. 否则,如果是可重入获取或者队列策略允许,则当前线程有资格获取锁。若满足条件,则更新锁状态并设置锁的持有者。
*
* @param acquires 要获取的锁的数量
* @return 如果成功获取锁则返回 true,否则返回 false
* @throws Error 如果锁计数将超过最大限制
*/
protected final boolean tryAcquire(int acquires) {
/*
* 步骤说明:
* 1. 如果读锁计数不为零或者写锁计数不为零
* 且当前线程不是锁的持有者,则获取失败。
* 2. 如果锁计数将达到饱和,则获取失败。(此情况仅在计数已经不为零时可能发生)
* 3. 否则,当前线程有资格获取锁,条件是
* 这是一次可重入获取或者队列策略允许。如果满足条件,则更新锁状态并设置锁的持有者。
*/
// 获取当前线程
Thread current = Thread.currentThread();
// 获取当前锁的状态
int c = getState();
// 获取当前写锁的持有计数
int w = exclusiveCount(c);
// 如果锁的状态不为零,说明已经有锁被持有
if (c != 0) {
// (注意: 如果 c != 0 且 w == 0 则说明读锁计数不为零)
// 如果写锁计数为零或者当前线程不是锁的持有者,获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 如果写锁计数加上要获取的锁数量超过最大限制,抛出错误
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 可重入获取,更新锁状态
setState(c + acquires);
return true;
}
// 如果写锁获取应该阻塞或者 CAS 操作更新状态失败,获取失败
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 设置当前线程为锁的持有者
setExclusiveOwnerThread(current);
return true;
}
写锁释放锁流程
/**
* 尝试释放写锁。
* 如果当前线程不是写锁的持有者,则抛出 IllegalMonitorStateException 异常。
* 如果释放后写锁的持有计数变为 0,则将锁的持有者设置为 null,并将锁标记为可用。
*
* @param releases 要释放的锁的数量。
* @return 如果释放后写锁不再被持有,则返回 true;否则返回 false。
* @throws IllegalMonitorStateException 如果当前线程不是写锁的持有者。
*/
protected final boolean tryRelease(int releases) {
// 检查当前线程是否是写锁的持有者,如果不是则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 计算释放锁后的状态
int nextc = getState() - releases;
// 判断释放后写锁是否不再被持有
boolean free = exclusiveCount(nextc) == 0;
// 如果写锁不再被持有,则将锁的持有者设置为 null
if (free)
setExclusiveOwnerThread(null);
// 更新锁的状态
setState(nextc);
// 返回写锁是否不再被持有的结果
return free;
}
读锁加锁流程
/**
* 以共享模式获取锁,忽略中断。首先尝试调用 {@link #tryAcquireShared} 方法获取锁,
* 如果成功(返回值大于等于 0)则直接返回;否则,将当前线程加入等待队列,
* 并可能会多次阻塞和唤醒,直到成功获取锁。
*
* @param arg 获取锁时传递的参数。此值会被传递给 {@link #tryAcquireShared} 方法,
* 但除此之外不做其他解释,可以表示任意你想要的值。
*/
public final void acquireShared(int arg) {
// 尝试以共享模式获取锁,如果返回值小于 0 表示获取失败
if (tryAcquireShared(arg) < 0)
// 调用 doAcquireShared 方法将当前线程加入等待队列并尝试重新获取锁
doAcquireShared(arg);
}
==>
/**
* 尝试以共享模式获取锁。此方法是非阻塞的,会尝试立即获取锁。
*
* 操作步骤如下:
* 1. 如果写锁被其他线程持有,则获取失败。
* 2. 否则,此线程在状态上有资格获取锁,因此检查是否因队列策略而应阻塞。
* 如果不应阻塞,则尝试通过 CAS 操作更新状态并增加计数来授予锁。
* 注意,此步骤不检查重入获取,这会推迟到完整版本中进行,以避免在更常见的非重入情况下检查持有计数。
* 3. 如果步骤 2 失败,无论是因为线程显然不符合资格、CAS 操作失败还是计数饱和,
* 则调用完整版本的方法进行重试。
*
* @param unused 此参数未使用,为了与 AQS 的 API 保持一致
* @return 如果获取成功,返回一个正数;如果由于写锁被其他线程持有而失败,返回 -1;
* 如果需要进一步重试,则调用 fullTryAcquireShared 方法。
*/
protected final int tryAcquireShared(int unused) {
/*
* 操作步骤说明:
* 1. 如果写锁被其他线程持有,则获取失败。
* 2. 否则,此线程在状态上有资格获取锁,因此检查是否因队列策略而应阻塞。
* 如果不应阻塞,则尝试通过 CAS 操作更新状态并增加计数来授予锁。
* 注意,此步骤不检查重入获取,这会推迟到完整版本中进行,以避免在更常见的非重入情况下检查持有计数。
* 3. 如果步骤 2 失败,无论是因为线程显然不符合资格、CAS 操作失败还是计数饱和,
* 则调用完整版本的方法进行重试。
*/
// 获取当前线程
Thread current = Thread.currentThread();
// 获取当前锁的状态
int c = getState();
// 如果写锁被其他线程持有,则获取失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取当前共享锁的持有计数
int r = sharedCount(c);
// 如果不需要阻塞,且共享锁计数未达到最大值,且 CAS 操作成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 如果是第一个获取共享锁的线程
if (r == 0) {
// 设置第一个读者为当前线程
firstReader = current;
// 第一个读者的持有计数为 1
firstReaderHoldCount = 1;
// 如果当前线程就是第一个读者
} else if (firstReader == current) {
// 第一个读者的持有计数加 1
firstReaderHoldCount++;
// 其他情况
} else {
// 获取缓存的持有计数器
HoldCounter rh = cachedHoldCounter;
// 如果缓存的持有计数器为空或者不是当前线程的持有计数器
if (rh == null || rh.tid != getThreadId(current))
// 从 ThreadLocal 中获取持有计数器
cachedHoldCounter = rh = readHolds.get();
// 如果持有计数为 0
else if (rh.count == 0)
// 将持有计数器设置到 ThreadLocal 中
readHolds.set(rh);
// 持有计数加 1
rh.count++;
}
// 返回 1 表示获取成功
return 1;
}
// 如果上述步骤失败,调用完整版本的方法进行重试
return fullTryAcquireShared(current);
}
非公平锁的判断
/**
* 检查队列中明显的第一个线程是否以独占模式等待。
* 如果此方法返回 {@code true},并且当前线程正在尝试以共享模式获取锁(即此方法从 {@link #tryAcquireShared} 调用),
* 则可以保证当前线程不是队列中的第一个线程。此方法仅在 ReentrantReadWriteLock 中用作启发式方法。
*
* @return 如果队列中明显的第一个线程以独占模式等待,则返回 {@code true};否则返回 {@code false}。
*/
final boolean apparentlyFirstQueuedIsExclusive() {
// 声明两个节点引用 h 和 s,分别用于表示队列的头节点和头节点的下一个节点
Node h, s;
// 检查头节点是否不为空,头节点的下一个节点是否不为空,
// 头节点的下一个节点是否不是以共享模式等待,并且该节点关联的线程是否不为空
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
读锁的释放锁流程
/**
* 以共享模式释放锁。如果{@link #tryReleaseShared}返回{@code true},则调用{@link #doReleaseShared}方法来唤醒等待的线程。
* 这个方法可以用来实现{@link Lock}接口中的解锁操作。
*
* @param arg 释放锁的参数。这个值会被传递给{@link #tryReleaseShared}方法,但除此之外没有其他含义,可以表示任何你想要的值。
* @return 如果成功释放锁则返回{@code true},否则返回{@code false}。
*/
public final boolean releaseShared(int arg) {
// 尝试以共享模式释放锁
if (tryReleaseShared(arg)) {
// 如果释放成功,则执行共享释放操作,唤醒等待的线程
doReleaseShared();
return true;
}
// 释放失败,返回false
return false;
}
==>
/**
* 尝试释放共享锁。此方法会减少当前线程持有的读锁计数,
* 并在必要时更新内部状态以反映锁的释放。
*
* @param unused 此参数未被使用,仅为了与 {@link AbstractQueuedSynchronizer} 中的方法签名保持一致。
* @return 如果释放操作导致所有读锁都被释放(即读锁计数为 0),则返回 true;否则返回 false。
*/
protected final boolean tryReleaseShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 检查当前线程是否是第一个获取读锁的线程
if (firstReader == current) {
// 断言第一个读者的持有计数大于 0
// assert firstReaderHoldCount > 0;
// 如果第一个读者的持有计数为 1
if (firstReaderHoldCount == 1)
// 将第一个读者置为 null
firstReader = null;
else
// 否则,将第一个读者的持有计数减 1
firstReaderHoldCount--;
} else {
// 获取缓存的持有计数器
HoldCounter rh = cachedHoldCounter;
// 如果缓存的持有计数器为空或者线程 ID 不匹配
if (rh == null || rh.tid != getThreadId(current))
// 从 ThreadLocal 中获取持有计数器
rh = readHolds.get();
// 获取当前线程的持有计数
int count = rh.count;
// 如果持有计数小于等于 1
if (count <= 1) {
// 从 ThreadLocal 中移除持有计数器
readHolds.remove();
// 如果持有计数小于等于 0,抛出异常
if (count <= 0)
throw unmatchedUnlockException();
}
// 将持有计数减 1
--rh.count;
}
// 循环尝试更新状态
for (;;) {
// 获取当前的同步状态
int c = getState();
// 计算释放后的同步状态
int nextc = c - SHARED_UNIT;
// 使用 CAS 操作尝试更新同步状态
if (compareAndSetState(c, nextc))
// 释放读锁对读者没有影响,但如果读写锁现在都空闲了,可能允许等待的写者继续执行
return nextc == 0;
}
}
==>
/**
* 释放共享锁,并确保释放操作能够传播给后续等待的线程,即使在释放过程中有其他线程正在进行获取或释放操作。
* 此方法会尝试唤醒头节点的后继节点,如果头节点需要信号唤醒的话。如果头节点不需要信号唤醒,
* 则将其状态设置为PROPAGATE,以确保在后续释放操作时能继续传播。
* 此外,为了处理在操作过程中可能有新节点加入队列的情况,需要进行循环操作。
* 与其他使用unparkSuccessor的方法不同,这里需要知道CAS操作重置状态是否失败,如果失败则需要重新检查。
*/
private void doReleaseShared() {
/*
* 确保释放操作能够传播,即使有其他正在进行的获取/释放操作。
* 通常的做法是,如果头节点需要信号唤醒,则尝试唤醒其后续节点。
* 但如果不需要信号唤醒,则将状态设置为PROPAGATE,以确保在释放时传播继续。
* 此外,由于在操作过程中可能有新节点加入队列,因此需要进行循环。
* 与其他使用unparkSuccessor的情况不同,这里需要知道CAS操作重置状态是否失败,如果失败则需要重新检查。
*/
for (;;) {
// 获取当前队列的头节点
Node h = head;
// 检查头节点不为空且头节点不是尾节点
if (h != null && h != tail) {
// 获取头节点的等待状态
int ws = h.waitStatus;
// 如果头节点的等待状态为SIGNAL,说明后继节点需要被唤醒
if (ws == Node.SIGNAL) {
// 尝试将头节点的等待状态从SIGNAL改为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // 如果CAS操作失败,继续循环重新检查
// 唤醒头节点的后继节点
unparkSuccessor(h);
}
// 如果头节点的等待状态为0,尝试将其状态设置为PROPAGATE
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // 如果CAS操作失败,继续循环
}
// 如果头节点在操作过程中没有发生变化,则退出循环
if (h == head)
break;
}
}