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 lockrwl.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 lockrwl.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;// 如果写锁不再被持有,则将锁的持有者设置为 nullif (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;// 第一个读者的持有计数为 1firstReaderHoldCount = 1;// 如果当前线程就是第一个读者} else if (firstReader == current) {// 第一个读者的持有计数加 1firstReaderHoldCount++;// 其他情况} else {// 获取缓存的持有计数器HoldCounter rh = cachedHoldCounter;// 如果缓存的持有计数器为空或者不是当前线程的持有计数器if (rh == null || rh.tid != getThreadId(current))// 从 ThreadLocal 中获取持有计数器cachedHoldCounter = rh = readHolds.get();// 如果持有计数为 0else if (rh.count == 0)// 将持有计数器设置到 ThreadLocal 中readHolds.set(rh);// 持有计数加 1rh.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;}// 释放失败,返回falsereturn 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;// 如果第一个读者的持有计数为 1if (firstReaderHoldCount == 1)// 将第一个读者置为 nullfirstReader = null;else// 否则,将第一个读者的持有计数减 1firstReaderHoldCount--;} else {// 获取缓存的持有计数器HoldCounter rh = cachedHoldCounter;// 如果缓存的持有计数器为空或者线程 ID 不匹配if (rh == null || rh.tid != getThreadId(current))// 从 ThreadLocal 中获取持有计数器rh = readHolds.get();// 获取当前线程的持有计数int count = rh.count;// 如果持有计数小于等于 1if (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改为0if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue; // 如果CAS操作失败,继续循环重新检查// 唤醒头节点的后继节点unparkSuccessor(h);}// 如果头节点的等待状态为0,尝试将其状态设置为PROPAGATEelse if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue; // 如果CAS操作失败,继续循环}// 如果头节点在操作过程中没有发生变化,则退出循环if (h == head)break;}
}
相关文章:
Java多线程与高并发专题——深入ReentrantReadWriteLock
深入ReentrantReadWriteLock 读写锁出现原因 synchronized和ReentrantLock都是互斥锁。如果说有一个操作是读多写少的,还要保证线程安全的话。如果采用上述的两种互斥锁,效率方面很定是很低的。在这种情况下,咱们就可以使用ReentrantReadWr…...
支付宝 IoT 设备入门宝典(下)设备经营篇
上篇介绍了支付宝 IoT 设备管理,但除了这些基础功能外,商户还可以利用设备进行一些运营动作,让设备更好的帮助自己,本篇就会以设备经营为中心,介绍常见的设备相关能力和问题解决方案。如果对上篇感兴趣,可以…...
极简本地体验deepseek大模型教程
一 题外随感:时代之问 就像狄更斯在双城记中所述,“这是最好的时代,这是最坏的时代”。每一代人都有其所处的时代,每一个时代都有其所谓好的一面和不那么好的一面。很多时候随口的一句大环境不好,就似乎给了自己一个最…...
最短路问题--Floyd
Floyd算法 一、介绍二、补充知识:邻接矩阵三、原理四、实现 提示:以下是本篇文章正文内容,下面案例可供参考 一、介绍 Floyd算法是一种用来计算图中所有点之间最短距离的算法。它的核心思想是:通过逐步尝试每个点作为中间点&…...
深入理解Java网络编程:从基础到高级应用
一、网络编程概述 1.什么是网络编程? 网络编程是指利用计算机网络实现程序之间通信的一种编程方式。在网络编程中,程序需要通过网络协议(如 TCP/IP)来进行通信,以实现不同计算机之间的数据传输和共享。 2.在网络编程…...
浅谈deepseek环境搭建
在探索人工智能的浩瀚宇宙中,DeepSeek如同一颗璀璨的星辰,以其独特的魅力引领着我们在逻辑推理与数据分析的海洋中遨游。想要在这片未知的领域里扬帆起航,首先必须精心搭建起我们的“星际飞船”——DeepSeek环境。无论你是渴望在本地实例上运…...
AI绘画软件Stable Diffusion详解教程(2):Windows系统本地化部署操作方法(专业版)
一、事前准备 1、一台配置不错的电脑,英伟达显卡,20系列起步,建议显存6G起步,安装win10或以上版本,我的显卡是40系列,16G显存,所以跑大部分的模型都比较快; 2、科学上网࿰…...
kali liux的下载
Kali Linux | Penetration Testing and Ethical Hacking Linux Distributionhttps://www.kali.org/ VMware虚拟机https://pan.quark.cn/s/aa869ffbf184 【补充一个今天学到的知识昂和内容无关:(遥感)指非接触的远距离探测技术,使用传感器探…...
DeepSeek 助力 Vue3 开发:打造丝滑的悬浮按钮(Floating Action Button)
前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏关注哦 💕 目录 Deep…...
ES from size聚合查询10000聚合查询,是每个分片先聚合,再统计。还是所有节点查询1万条后,再聚合
在 Elasticsearch 中,聚合查询 的执行过程是 分布式 的,Elasticsearch 会先在每个分片(shard)上执行本地聚合,然后再在协调节点(coordinating node)上对所有分片的聚合结果进行 全局汇总。具体过…...
fluent-ffmpeg 依赖详解
fluent-ffmpeg 是一个用于在 Node.js 环境中与 FFmpeg 进行交互的强大库,它提供了流畅的 API 来执行各种音视频处理任务,如转码、剪辑、合并等。 一、安装 npm install fluent-ffmpeg二、基本使用 要使用 fluent-ffmpeg,首先需要确保系统中…...
SLAM文献之-DROID-SLAM: Deep Visual SLAM for Monocular, Stereo, and RGB-D Cameras
DROID-SLAM 是一种结合深度学习与传统视觉SLAM技术的先进算法,其核心目标是通过端到端可训练的深度神经网络来实现高精度的相机位姿估计和稠密三维重建。与传统SLAM方法不同,DROID-SLAM采用深度学习网络来估计深度信息,提供更高的精度与鲁棒性…...
一、旋转编码器模块分析与使用
一、旋转编码器说明 该模块配合定时器的encoder使用时,可通过旋转来进行调整记录编码的数值。(通过旋转编码器的数值与字母建立对应关系,即可进行打字编码) 引脚说明: vcc,gnd,供电使用 sw&am…...
【踩坑日志】解决CU118环境下RuntimeError: NCCL error: invalid usage
本博客主要记录了CU118环境下,出现报错信息为RuntimeError: NCCL error: invalid usage的解决方案。我的环境信息如下: cuda版本:11.7torch版本:torch-2.5.1-cu118 定位到核心报错信息为: NCCL WARN NCCL cannot be …...
FREERTOS的三种调度方式
一、调度器的调度方式 调度器的调度方式解释针对的对象抢占式调度1.高优先级的抢占低优先级的任务 2.高优先级的任务不停止,低优先级的任务不能执行 3.被强占的任务会进入就绪态优先级不同的任务时间片调度1.同等优先级任务轮流享用CPU时间 2.没有用完的时间片&…...
338.比特位计数<动态规划>
338. 比特位计数 - 力扣(LeetCode) class Solution { public:vector<int> countBits(int n) {//将所有数初始化为0vector<int>dp(n1,0);for(int i 0; i<n;i){if(i % 2 0){dp[i] dp[i/2];}else{dp[i] dp[i/2]1;}}return dp;} };...
释放你的IDE潜能:Code::Blocks 插件创意开发深度指南
释放你的IDE潜能:Code::Blocks 插件创意开发深度指南 在软件开发的浩瀚世界中,集成开发环境 (IDE) 扮演着至关重要的角色。一款优秀的 IDE 不仅能提升开发效率,更能激发开发者的创造力。Code::Blocks,作为一款开源、跨平台的 C, C++ 和 Fortran IDE,以其轻量级、高度可定…...
行星际激波与高能粒子的相互作用机制及其天体物理意义
第一章 行星际激波的物理本质与形成机制 1.1 激波的普遍定义与分类 激波(Shock Wave)是介质中传播的压缩性不连续面,其本质是介质参数(如密度、速度、压力)的突变。在天体物理中,根据激波传播方向与磁场…...
C# 牵手DeepSeek:打造本地AI超能力
一、引言 在人工智能飞速发展的当下,大语言模型如 DeepSeek 正掀起新一轮的技术变革浪潮,为自然语言处理领域带来了诸多创新应用。随着数据隐私和安全意识的提升,以及对模型部署灵活性的追求,本地部署 DeepSeek 成为众多开发者和…...
不同版本的BLE和WiFi有什么区别?
一、蓝牙技术对比:从 Bluetooth 4.0 到 5.3 的演进与室内定位应用 蓝牙技术自推出以来,经历了多次重大升级,每一代都在传输速率、功耗、覆盖范围和功能上有所改进。本文将从 Bluetooth 4.0 到 5.3,逐一对比各版本的特点࿰…...
LVS+Keepalived高可用高性能负载实战
高可用集群( High Availability Cluster, HA 集群),其中高可用的含义是最大限度地可以使用。从集群 的名字上可以看出,此类集群实现的功能是保障用户的应用程序持久、不间断地提供服务。 当应用程序出现故障或者系统硬件、网络出现…...
网络安全-使用DeepSeek来获取sqlmap的攻击payload
文章目录 概述DeepSeek使用创建示例数据库创建API测试sqlmap部分日志参考 概述 今天来使用DeepSeek做安全测试,看看在有思路的情况下实现的快不快。 DeepSeek使用 我有一个思路,想要测试sqlmap工具如何dump数据库的: 连接mysql数据库&#…...
【MongoDB】在Windows11下安装与使用
官网下载链接:Download MongoDB Community Server 官方参考文档:https://www.mongodb.com/zh-cn/docs/manual/tutorial/install-mongodb-on-windows/#std-label-install-mdb-community-windows 选择custom类型,其他默认 注意,此选…...
vscode输入!+tab没反应??
!+tab直接生成html框架 第一步 ctrlshipp 选择更改语言模式 change language mode, 选择HTML 然后试一下行不行,如果还不行看第二步 第二步 检查一下输入的!是不是英文输入法输入的,一定要是英文输入&…...
【Cadence射频仿真学习笔记】2.4GHz低噪放LNA仿真设计
课程分为3个部分, 一、LNA结构与噪声优化方法 噪声优化的方法是:限定功耗的噪声和功率同时匹配噪声匹配和功率匹配一般不会同时达到, 对于PCSNIM结构的噪声分析,我们只需要了解与哪些参数有关优化思路是:1.信号源阻抗…...
初阶MySQL(两万字全面解析)
文章目录 1.初识MySQL1.1数据库1.2查看数据库1.3创建数据库1.4字符集编码和排序规则1.5修改数据库1.6删除数据库 2.MySQL常用数据类型和表的操作2.(一)常用数据类型1.数值类2.字符串类型3.二进制类型4.日期类型 2.(二)表的操作1查看指定库中所有表2.创建表 3.查看表结构和查看表…...
Python每日一练:学习指南进行汇总
Python,一种“优雅”、“明确”、“简单”的编程语言,凭借其低学习曲线、强大的开源生态系统、卓越的平台可移植性以及面向对象和函数式编程的支持,成为了众多开发者首选。 01 Python 应用领域和就业形势分析 Python,一种“优雅…...
Spring-AI搭建企业专属知识库 一
环境介绍:Spring3.3.2 JDK 21 POM文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation&…...
Python的那些事第三十六篇:基于 Vega 和 Vega-Lite 的数据可视化解决方案,Altair 声明式可视化库
Altair 声明式可视化库:基于 Vega 和 Vega-Lite 的数据可视化解决方案 摘要 在数据科学和分析领域,有效的数据可视化是理解数据、发现模式和传达见解的关键。Python 作为数据科学的主要编程语言之一,提供了多种数据可视化库。其中,Altair 是一个基于 Vega 和 Vega-Lite 的…...
虚拟化园区网络部署指南
《虚拟化园区网络部署指南》属于博主的“园区网”专栏,若想成为HCIE,对于园区网相关的知识需要非常了解,更多关于园区网的内容博主会更新在“园区网”专栏里,请持续关注! 一.前言 华为CloudCampus解决方案基于智简网络…...
系统调用有哪些函数
系统调用是操作系统提供给用户程序的一组“特殊”的函数接口,允许用户程序请求操作系统执行某些低级服务。这些服务通常涉及对硬件的直接操作或访问受保护的内核资源。以下是一些常见的系统调用函数,主要基于Unix/Linux环境: 一、文件与设备…...
Go红队开发—编解码工具
文章目录 开启一个项目编解码工具开发Dongle包Base64编解码摩斯密码URL加解密AES加解密 MD5碰撞工具开发 开启一个项目 这作为补充内容,可忽略直接看下面的编解码: 一开始用就按照下面的步骤即可 1.创建一个文件夹,你自己定义名字(建议只用…...
PyInstaller 打包python 程序 成 可执行文件
pyinstaller --onefile --name my_project --add-data "config/config.json:config" main.py 要将整个 Python 项目打包成一个可执行文件,可以使用 PyInstaller 来完成这个任务。以下是如何将整个项目打包成可执行文件的步骤: 1. 安装 PyIns…...
2继续NTS库学习(读取shapefile)
引用库如下: 读取shapefile代码如下: namespace IfoxDemo {public class Class1{[CommandMethod("xx")]public static void nts二次学习(){Document doc Application.DocumentManager.MdiActiveDocument;var ed doc.Editor;string shpPath …...
Python爬虫
python凭借其简洁的语法和强大的库支持,成为编写爬虫程序的首选语言之一。今天,我将通过一个简单的示例,带你入门Python爬虫,并展示如何爬取网页内容并保存到文本文件中。 一、爬虫的基本概念 爬虫(Web Crawler&#…...
C++蓝桥杯基础篇(六)
片头 嗨~小伙伴们,大家好!今天我们来一起学习蓝桥杯基础篇(六),练习相关的数组习题,准备好了吗?咱们开始咯! 第1题 数组的左方区域 这道题,实质上是找规律,…...
rust学习~tokio的io
await Suspend execution until the result of a Future is ready. 暂停执行,直到一个 Future 的结果就绪。 .awaiting a future will suspend the current function’s execution until the executor has run the future to completion. 对一个 Future 使用 .awa…...
JVM--虚拟机
JVM,即虚拟机,可以简单理解为将字节码文件翻译成机器码的机器。 .class文件-->机器码文件 JVM整体组成部分 1.类加载器 负责从磁盘中加载字节码文件到JVM中 2.运行时数据区 按照不同的数据分区进行存储(方法区,堆,栈,本地方…...
【Unity】把Texture的黑色背景改成透明背景
1. 在Project窗口中选择目标Texture 2. 在Inspector窗口中进行如下设置: Texture Type: Sprite (2D and UI)Alpha Source: Input Texture Alpha (如果原图有Alpha通道) 或 From Gray Scale (如果要用灰度值作为透明度)Alpha Is Transparency: ✓ (勾选) 3. 其他建…...
计算机毕业设计SpringBoot+Vue.js华强北商城二手手机管理系统 (源码+文档+PPT+讲解)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
利用 Python 爬虫进行跨境电商数据采集
1 引言2 代理IP的优势3 获取代理IP账号4 爬取实战案例---(某电商网站爬取)4.1 网站分析4.2 编写代码4.3 优化代码 5 总结 1 引言 在数字化时代,数据作为核心资源蕴含重要价值,网络爬虫成为企业洞察市场趋势、学术研究探索未知领域…...
Android中使用Robolectric测试点击事件(不需要手机)
文章目录 一、前言二、简单示例三、参考文档 一、前言 Robolectric 是一个由 Google 维护的开源 Android 测试框架,它允许你以 Android 运行时环境运行单元测试。 Robolectric 提供了一个模拟 Android 运行时环境,允许你测试你的代码是否正确地使用 And…...
如何把网络ip改为动态:全面指南
在数字化时代,网络IP地址作为设备在网络中的唯一标识,扮演着至关重要的角色。随着网络环境的不断变化,静态IP地址的局限性逐渐显现,而动态IP地址则因其灵活性和安全性受到越来越多用户的青睐。那么,如何把网络IP改为动…...
文件描述符与重定向
1. open系统调用 在 Linux 中, open() 系统调用用于打开一个文件或设备,并返回一个文件描述符,通过该描述符可以进行文件读写操作。open() 可以用于创建新文件或打开已存在的文件,具体行为取决于传递给它的参数。 需要包含的头文件…...
自然语言处理NLP入门 -- 第六节命名实体识别
1 什么是命名实体识别? 在日常生活中,我们经常会遇到这样的情景:希望从一大段文本中,快速找出所有的人名、地名、组织机构名称、日期、时间等关键信息。举个例子,如果你在阅读一篇关于历史事件的新闻报道时࿰…...
Windows PicPick Professional-v7.3.2-中文版
Windows PicPick Professional-中文版 链接:https://pan.xunlei.com/s/VOKGwGVGWUDl7L8cW4D1A1W4A1?pwdw5qz# - 更新了中文翻译,默认取消检测升级,删除多国语言...
Hue UI展示中文
个人博客地址:Hue UI展示中文 | 一张假钞的真实世界 如果使用开发分支代码如master分支)编译安装,需要自己编译语言文件。例如Hue安装目录为“/opt/hue”,则安装后执行以下命令: $ cd /opt/hue $ make locales 如果…...
【Unity】AI Navigation自动寻路(导航)功能
1.简介以及安装AI Navigation 1.1 简介 AI导航包包含高级组件,允许你在游戏中使用导航网格来整合导航和寻径。有了这个包,你可以在运行时和编辑时构建和使用导航网格,创建动态障碍,并使用链接来允许特定的动作(如跳跃…...
网络安全员证书
软考网络安全员证书:信息安全领域的黄金标准 随着信息技术的飞速发展,网络安全问题日益凸显,网络安全员的需求也日益增加。软考网络安全员证书作为信息安全领域的黄金标准,对于网络安全从业者来说具有重要意义。本文将详细介绍…...
2.你有什么绝活儿?—Java能做什么?
1、Java的绝活儿:要问Java有什么绝活,我觉得它应该算是一位魔法师,会的绝活儿有很多,要说最能拿得出手的当属以下三个。 1.1 平台无关性:Java可以在任何地方施展魔法,无论是Windows、Linux还是Mac…...