本文主要内容是对并发包中的读写锁的认识,主要解释读写锁的请求过程,锁降级的实现以及锁升级的不可能性。
首先来了解一些常量和简单方法,贴下代码
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_UNIT=65536,MAX_COUNT=EXCLUSIVE_MASK=65535
如果用二进制来表示SHARED_UNIT的第17位是1,后面16个0;65535的前面16位都是1。sharedCount(int c)方法
计算的是二进制中17位起的值,exclusiveCount(int c)计算的是c前面16位的值。在读写锁的源码中,读取锁和写入锁的共有一个state变量计算,写入线程获取锁将state变量加1(在state的低16位内操作),读取线程获取锁将state变量加65536(在state的17位起操作的),看看代码中的英文解释可以理解大概意思,读取线程获取锁的方式是共享模式的,而写入线程的是独占模式的,下面通过代码来认识它的特点。
先来认识下,读取线程的请求锁代码
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail
* 2. If count saturated, throw error
* 3. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 4. If step 3 fails either because thread
* apparently not eligible or CAS fails,
* chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (!readerShouldBlock(current) &&
compareAndSetState(c, c + SHARED_UNIT)) {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
rh.count++;
return 1;
}
return fullTryAcquireShared(current);
}
代码的逻辑已经有详细的英文解释了,在这里说点别的:
exclusiveCount(c)方法它返回的是低16位的值,更彻底的说是只关心低16位的值,也就是说是否有写入线程持有锁
,如果持有的话就失败;sharedCount(c)方法是将c右移16位,返回的17位起的值,如果返回值为65535,则请求失败。再来看看readerShouldBlock(current)方法,这个方法在AQS中的关键代码如下
/**
* Return {@code true} if the apparent first queued thread, if one
* exists, is not waiting in exclusive mode. Used only as a heuristic
* in ReentrantReadWriteLock.
*/
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return ((h = head) != null && (s = h.next) != null &&
s.nextWaiter != Node.SHARED);
}
同步队列中的第一个等待者是独占模式的(在这里只有独占模式和共享模式)情况就返回true,否则返回false,换句话说就是第一个等待者是写入线程,它就返回true,即读线程应该阻塞,否则读线程不应该阻塞,这里是降级锁实现的关键。
在这里先来对Node节点中的nextWaiter域的应用做个总结(可以结合另一边文章关于ReentrantLock中condition的理解)nextWaiter有两个作用:一、在共享模式中做标记作用,独占模式是static final Node EXCLUSIVE = null;共享模式是static final Node SHARED = new Node();在来看看Node的一个构造方法
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
所以nextWaiter在这里起标记作用,其中的共享模式是一个空的节点;
二、在条件队列中做指针,但在条件队列中节点的构造方法采用的是
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
其中waitStatus=-2,nextWaiter用来指向实际的“条件节点”(由上述构造方法产生的节点),在实际使用中不会产生歧义的。在回到readerShouldBlock(current)方法,意义就是同步队列中第一个等待者是写入线程就返回true。
现在回到读取线程的请求方法tryAcquireShared(int unused)中,来看看readHoldsd的作用,在源码中的解释如下
/**
* The number of read locks held by current thread.
* Initialized only in constructor and readObject.
*/
transient ThreadLocalHoldCounter readHolds;
翻译为当前线程的读取锁计数,这是因为读取锁是共享模式的,多个读取线程都可以进行compareAndSetState(c, c +SHARED_UNIT)操作,同时单个线程又是可重入的,所以要记录每个线程的读取锁记录;写入锁由于是独占模式的,所以没有这个问题。
下面来说下锁降级的实现,读写锁的锁降级指的是:在持有写入锁时,再去持有读取锁,然后释放写入锁,此时还持有读取锁。首先写入线程正常获取独占锁,在读取线程请求锁的时候,方法readerShouldBlock(current)返回false(因没有等待者),最后tryAcquireShared(int unused)方法返回1,获得许可,此时的state=65537,线程同时拥有读写锁,写入锁释放后,线程仍然持有读取锁。以下是锁降级的示例代码
// 锁降级:首先获取写入锁,然后获得读取锁,释放写入锁,释放读取锁
public String putAndGet(String key,String value){
String s="";
w.lock();
try{
s=m.put(key, value);
r.lock();
m.get(key);
} finally{
w.unlock();
r.unlock();
}
return s;
}
注意:如果是先获得读取锁,在获得写入锁,线程将被park,并且其他线程将不能获得任何一个锁,这是为什么?
先来看看写入线程请求锁的代码
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)
if (w == 0 || current != getExclusiveOwnerThread())
return false; // 标记1
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if ((w == 0 && writerShouldBlock(current)) ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
代码会在标记1处由于c=65536,w=0,当前线程不是独占线程而返回false,请求写入锁失败。再来看看读取线程是怎么失败的,程序会走到fullTryAcquireShared方法,代码如下
/**
* Full version of acquire for reads, that handles CAS misses
* and reentrant reads not dealt with in tryAcquireShared.
*/
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
rh = readHolds.get();
for (;;) {
int c = getState();
int w = exclusiveCount(c);
if ((w != 0 && getExclusiveOwnerThread() != current) ||
((rh.count | w) == 0 && readerShouldBlock(current)))
return -1; //标记2
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
cachedHoldCounter = rh; // cache for release
rh.count++;
return 1;
}
}
}
它在标记2处返回-1,表示读线程获取锁失败,将进入同步队列。所以从读取锁升级写入锁是不可能的。
最后就是锁的释放,在理解了本文,相信看读写锁的释放操作还是比较简单的。
分享到:
相关推荐
通过理解ReentrantReadWriteLock的工作原理和设计,开发者可以更好地在多线程环境中控制并发访问,提高程序的效率和安全性。在面试或实际开发中,掌握这些知识点对于解决复杂并发问题是至关重要的。
6.5 深入理解 AQS之 ReentrantReadWritelock 实战副本.mp4
6.5 深入理解 AQS之 ReentrantReadWritelock 实战副本副本.mp4
总之,ReentrantReadWriteLock是Java并发编程中非常重要的一个工具,理解其内部机制有助于编写更高效、更可靠的多线程程序。通过深入研究源码,我们可以更好地掌握并发控制的策略,以及如何在实际项目中应用这些策略...
内容包括 01-并发编程之深入理解JMM&并发三大特性(一)-fox 02-并发编程之深入理解JMM&并发三...11-深入理解AQS之CyclicBarrier&ReentrantReadWriteLock详解-fox 12-深入理解AQS之ReentrantReadWriteLock详解-fox ...
在Java中,ReentrantReadWriteLock类是读写锁的实现,它包含两个锁:读锁(共享锁)和写锁(独占锁)。读锁可以被多个线程同时持有,而写锁是独占的,当写锁被占用时,其他线程既不能获取读锁也不能获取写锁。 5. ...
总之,`ReentrantLock`和`ReentrantReadWriteLock`提供了比`synchronized`更精细的锁控制,而争用分析则帮助我们理解和优化这些锁的使用。`contention-profiling-master`这个项目很可能是用来演示如何进行此类分析的...
11.深入理解读写锁ReentrantReadWriteLock 12.详解Condition的await和signal等待通知机制 13.LockSupport工具 14.并发容器之ConcurrentHashMap(JDK 1.8版本) 15.并发容器之ConcurrentLinkedQueue 16.并发容器之...
总结来说,Java中的并发加锁机制非常灵活且强大,通过对`ReentrantLock`和`ReentrantReadWriteLock`等工具的理解和应用,可以有效地解决多线程环境下的并发控制问题。希望本文能帮助读者更好地理解和掌握Java并发...
3. Lock接口:了解ReentrantLock、ReentrantReadWriteLock等高级锁的使用。 六、IO流 1. 文件操作:理解File类的基本操作,如创建、删除、重命名文件。 2. 字节流与字符流:掌握InputStream、OutputStream、Reader...
3. Lock接口:了解ReentrantLock、ReentrantReadWriteLock的使用,与synchronized的区别。 五、IO与NIO 1. 流:掌握InputStream、OutputStream、Reader、Writer及其子类的使用。 2. NIO:理解非阻塞I/O的特点,...
- `ReentrantReadWriteLock`的源码提供了深入理解其工作原理的机会。例如,`readLock()`返回的`Sync`子类`NonReentrantReadLock`实现了读锁的逻辑,而`writeLock()`返回的`Sync`子类`NonReentrantWriteLock`实现了...
- Lock接口:ReentrantLock、ReentrantReadWriteLock等高级锁的使用。 5. **异常处理**: - 异常分类:了解检查异常和运行时异常的区别。 - try-catch-finally:理解异常捕获和处理,finally块的执行情况。 - ...
在Java并发编程中,读写锁是用于优化多线程访问共享资源的一种机制,它可以提高对数据的并发访问效率。本文将深入探讨Java中的两种读写锁...理解并正确使用这些锁机制,能够帮助我们编写出更加高效、安全的多线程程序。
首先,我们需要理解读者-写者问题的核心是读写锁。在Java中,我们可以使用`java.util.concurrent.locks`包下的`ReentrantReadWriteLock`类来实现。这个锁提供了一种方式,使得多个线程可以同时读取数据,但只允许一...
4. Lock接口:熟悉ReentrantLock、ReentrantReadWriteLock等高级锁的使用。 四、IO与NIO 1. 流的概念:掌握字节流和字符流,以及其对应的输入输出类。 2. 文件操作:熟练进行文件的创建、读写、复制、删除等操作。 ...
- **使用更高效的同步工具**:选择合适的数据结构和算法,比如使用`StampedLock`代替`ReentrantReadWriteLock`等。 #### 6. 错误处理 - **异常安全**:确保并发程序在出现异常时能够正确处理资源释放等问题。 - **...
4. 并发包:分析java.util.concurrent中的并发工具类,如ConcurrentHashMap、ThreadPoolExecutor、FutureTask、Semaphore、Condition、ReentrantReadWriteLock等。 5. JVM:深入理解JVM的工作机制,包括代码的编译、...
3. Lock接口:ReentrantLock、ReentrantReadWriteLock的使用。 4. 线程池:ExecutorService、ThreadPoolExecutor、Future接口的理解与使用。 六、IO流与NIO 1. 流的分类:字节流、字符流、输入流、输出流。 2. 文件...
在计算机科学和操作系统领域,"读者写者公平操作"是一个经典的多线程...理解和熟练应用这一机制对于开发高并发的Java应用程序至关重要。在实际项目中,根据具体需求选择公平或非公平策略,以达到性能和公平性的平衡。