本文主要内容是对并发包中的读写锁的认识,主要解释读写锁的请求过程,锁降级的实现以及锁升级的不可能性。
首先来了解一些常量和简单方法,贴下代码
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,表示读线程获取锁失败,将进入同步队列。所以从读取锁升级写入锁是不可能的。
最后就是锁的释放,在理解了本文,相信看读写锁的释放操作还是比较简单的。