1. 类继承关系
如图所示, ReadWriteLock 是一个接口,内部由两个Lock 接口组成。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock 实现了该接口,使用方式如下:
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock rLock = rwLock. readLock();
rLock.lock();
rLock.unlock();
Lock wLock = rwLock.writeLock();
wLock.lock();
wLock.unlock();
也就是说,当使用ReadWriteLock 的时候, 并不是直接使用,而是获得其内部的读锁和写锁,然后分别调用lock/unlock 。
2. 读写锁实现的基本原理
从表面来看, ReadLock 和WriteLock 是两把锁,实际上它只是同一把锁的两个视图而己。什么叫两个视图呢?可以理解为是一把锁,线程分成两类:读线程和写线程。读线程和读线程之间不互斥(可以同时拿到这把锁〉,读线程和写线程互斥,写线程和写线程也互斥。
从下面的构造函数也可以看出, readerLock 和writerLock 实际共用同一个sync 对象。sync对象同互斥锁一样,分为非公平和公平两种策略,井继承自AQS 。
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
同互斥锁一样,读写锁也是用state 变量来表示锁状态的。只是state 变量在这里的含义和互斥锁完全不同。在内部类Sync 中,对state 变量进行了重新定义,如下所示。
abstract static class Sync extends AbstractQueuedSynchronizer {
/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/
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; }
}
也就是把state 变量拆成两半,低16 位,用来记录写锁。但同一时间既然只能有一个线程写,为什么还需要16 位呢?这是因为一个写线程可能多次重入。例如,低16 位的值等于5 ,表示一个写线程重入了5 次。
高16 位,用来“读”锁。例如,高16 位的值等于5 ,可以表示5 个读线程都拿到了该锁;也可以表示一个读线程重入了5 次。
这个地方的设计很巧妙, 为什么要把一个int 类型变量拆成两半,而不是用两个int 型变量分别表示读锁和写锁的状态呢?这是因为无法用一次CAS 同时操作两个int 变量,所以用了一个int 型的高16 位和低16 位分别表示读锁和写锁的状态。
当state = 0 时,说明既没有线程持有读锁,也没有线程持有写锁:当state != 0 时,要么有线程持有读锁,要么有线程持有写锁,两者不能同时成立,因为读和写互斥。这时再进一步通过sharedCount(state)和exclusiveCount(state)判断到底是读线程还是写线程持有了该锁。
3. AQS 的两对模板方法
下面介绍在ReentrantReadWriteLock 的两个内部类ReadLock 和WriteLock中,是如何使用state 变量的。
public static class ReadLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
...
}
public static class WriteLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
...
}
acquire / release 、acquireShared/releaseShared 是AQS 里面的两对模板方法。互斥锁和读写锁的写暂且都是基于acquire /release 模板方法来实现盹读写锁的读暂且是基于acquireShared/ releaseShared这对模板方法来实现的。这两对模板方法的代码如下:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg) && // tryAcquire被各种Sync子类实现
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
public final boolean release(int arg) {
if (tryRelease(arg)) { // tryRelease被各种Sync子类实现
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // tryAcquireShared被各种Sync子类实现
doAcquireShared(arg);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // tryReleaseShared被各种Sync子类实现
doReleaseShared();
return true;
}
return false;
}
}
将读/写、公平/非公平进行排列组合,就有4 种组合。如图下图所示,上面的两个函数都是在Sync 中实现的。Sync 中的两个函数又是模板方法,在NonfairSync 和FairSync 中分别有实现。最终的对应关系如下:
(1)读锁的公平实现: Sync.tryAccquireShared() + FairSync 中的两个覆写的子函数。
(2)读锁的非公平实现: Sync.tryAccquireShared() + NonfairSync 中的两个覆写的子函数。
(3)写锁的公平实现: Sync.tryAccquire() + FairSync 中的两个覆写的子函数。
(4)写锁的非公平实现: Sync.tryAccquire() + NonfairSync 中的两个覆写的子函数。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() { // 写线程抢锁的时候是否应该阻塞,
return false; // 写线程在抢锁之前永远不被阻塞,是非公平的
}
final boolean readerShouldBlock() { // 读线程抢锁的时候是否应该阻塞,
return apparentlyFirstQueuedIsExclusive(); // 读线程抢锁的时侃当队列中第1个元素是写线程的时候,要阻塞
}
}
static final class FairSync extends Sync {
final boolean writerShouldBlock() { // 写线程抢锁的时候是否应该阻塞
return hasQueuedPredecessors(); // 写线程在抢锁之前,如果队列里有其他线程在排队,就要阻塞,所以是公平的
}
final boolean readerShouldBlock() { // 读线程抢锁的时候是否应该阻塞
return hasQueuedPredecessors(); // 读线程在抢锁之前,如果队列里有其他线程在排队,就要阻塞,所以是公平的
}
}
上面的代码介绍了ReentrantReadWriteLock 里面的NonfairSync 和FairSync 的实现过程,对应了上面的四种实现策略,下面分别解释。
对于公平,比较容易理解,不论是读锁,还是写锁,只要队列中有其他线程在排队(排队等读锁,或者排队等写锁),就不能直接去抢锁,要排在队列尾部。
对于非公平,读锁和写锁的实现策略略有差异。先说写锁,写线程能抢锁,前提是state= 0,只有在没有其他线程持有读锁或写锁的情况下,它才有机会去抢锁。或者state != 0 ,但那个持有写锁的线程是它自己,再次重入。写线程是非公平的,就是不管三七二十一就去抢,即一直
返回false 。
但对于读线程,能否也不管三七二十二,上来就去抢呢?不行!因为读线程和读线程是不互斥的,假设当前线程被读线程持有,然后其他读线程还非公平地一直去抢,可能导致写线程永远拿不到锁,所以对于读线程的非公平,要做一些“约束”。当发现队列的第l 个元素是写线程的时候,读线程也要阻塞一下, 不能“肆无忌惮”地直接去抢。
明白策略后,下面具体介绍四种实现方面的差异。
4. WriteLock公平与非公平实现
写锁是排他锁,实现策略类似于互斥锁,重写了tryAcquire/tryRelease 方法。
4.1 tryAcquire()实现分析
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;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
把上面的代码拆开进行分析,如下:
(1) if(c !=0) and w== 0 ,说明当前一定是读线程拿着锁,写锁一定拿不到,返回false ,
(2) if (c != 0) and w != 0 ,说明当前一定是写线程拿着锁,执行current != getExclusiveOwnerThread()的判断,发现ownerThread
不是自己,返回false 。
(3) c != o, w != 0 ,且current = getExclusiveOwnerThread(),才会走到if (w + exclusiveCount(acquires) > MAX_COUNT) 。
判断重入次数,重入次数超过最大值,抛出异常。
因为是用state 的低16位保存写锁重入次数的,所以MAXCOUNT 是2的16次方 。如果超出这个值, 会写到读锁的高16 位上。为了避免
这种情形,这里做了一个检测。当然, 一般不可能重入这么多次。
(4) if(c = 0),说明当前既没有读线程,也没有写线程持有该锁。可以通过CAS 操作开抢了。
if (writerShouldBlock () || !compareAndSetState(c, c + acquires))
抢成功后,调用setExclusiveOwnerThread(current),把ownerThread 设成自己。
公平实现和非公平实现几乎一模一样,只是writerShouldBlock()分别被FairSync 和NonfairSync 实现,在上一节己讲。
4.2 tryRelease(…)实现分析
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free) {
setExclusiveOwnerThread(null);
}
setState(nextc); // 因为写锁是排他的,在当前线程持有写锁的时候,其他线程既不会持有写锁,
// 不会持有读锁。所以,这里对state 值的调减不需要CAS 操作,直接减1即可
return free;
}
5. ReadLock公平与非公平实现
读锁是共享锁,重写了tryAcquireShared/tryReleaseShared方法,其实现策略和排他锁有很大的差异。
5.1 tryAcquireShared(…)实现分析
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 && // 写锁被某线程持有,并且这个线程还不是自己,读锁肯定拿不到,直接返回
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() && // 公平和非公平的差异就在于这个函数
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { // CAS 拿读锁,高16 位加1
if (r == 0) { // r 等于0 ,说明这是第一个拿到读锁的线程
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current); // 上面拿读锁失败,进入这个函数不断自旋拿读锁
}
(1)
if (exclusiveCount (c) != 0 && getExclusiveOwnerThread () != current)
return - 1 ;
低16 位不等于0 ,说明有写线程持有锁,并且只有当ownerThread != 自己时,才返回-1 。这里面有一个潜台词:如果current = ownerThread ,则这段代码不会返回。这是因为- 个写线程可以再次去拿读锁!也就是说, 一个线程在持有了WriteLock 后,再去调用ReadLock.lock 也是可以的。
(2) 上面的compareAndSetState(c, c + SHARED_ UNIT), 其实是把state 的高16 位加1 (读锁的状态),但因为是在高16 位,必须把1左移16 位再加1 。
(3) firstReader, cachedHoldConunter 之类的变量,只是一些统计变量, 在ReentrantReadWriteLock对外的一些查询函数中会用到,例如,查询持有读锁的线程列表,但对整个读写互斥机制没有影响,此处不再展开解释。
5.2 tryReleaseShared(…)实现分析
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
...
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
因为读锁是共享锁,多个线程会同时持有读锁,所以对读锁的释放不能直接减1 ,而是需要通过一个for 循环+ CAS 操作不断重试。这是tryReleaseShared 和tryRelease 的根本差异所在。