上次说了ReentrantLock这次说ReentrantReadWriteLock
解决线程安全问题使用ReentrantLock,但是ReentrantLock是独占锁,同一时刻只有一个线程可以获得锁,而实际中会有写多读少的场景,因此ReentrantLock满足不了这个需求,所以ReentrantReadWriteLock应运而生,ReentrantReadWriteLock采用读写分离策略,允许多个线程同时获得锁。
以下是ReentrantReadWriteLock的类图
从类图中我们可以看到ReentrantReadWriteLock中实现了WriteLock和ReadLock,依赖于了Sync实现具体功能(继承AQS来实现),同时也分为公平锁和非公平锁。
我们知道在AQS中维护了一个state状态值,通过状态值来判断锁的状态,而ReentrantReadWriteLock则需要维护读状态和写状态,那么一个state如何表示读状态和写状态呢,ReentrantReadWriteLock使用了state的高低位来实现,通过高16位表示读状态,也就是读锁的可重入次数,低16位表示写状态,也就是写锁的可重入次数。
static final int SHARED_SHIFT = 16;
//共享锁(读锁)状态单位值65536
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 共享锁线程最大个 65535
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//排它锁(写锁)掩码, 二进制, 15个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; }
其中firstReader用来记录第一个获取到锁的线程,firstReaderHoldCount则记录第一个获取到读锁的线程所获取的读锁的可重入次数,cachedHoldCounter用来记录最后一个获取读锁的线程所获取的读锁的可重入次数。
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
readHolds是ThreadLocal变量,用来存放除去第一个获取读锁线程外其它线程所获取的读锁的可重入次数。ThreadLocalHoldCounter继承ThreadLocal,因而initialValue返回一个HoldCounter对象。
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
写锁的获取与释放
写锁是一个独占锁,同一时刻只有一个线程可以获取该锁,如果没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回,如果已经有线程获取到了读锁和写锁,那么当前线程会被阻塞挂起。另外,写锁是可重入锁,如果当前线程已经获得了写锁,那么再次该线程再次获得写锁时会将state的值加1然后返回。
1、void lock()
//调用WriteLock中的lock方法
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
//sync重写tryAcquire方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如上代码中在lock内部调用AQS的acquire方法,其中tryAcquire是ReentrantReadWriteLock内部的sync类重写的,代码如下:
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);
//1、c!=0说明读锁或者写锁已经被线程获取
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//2、w==0说明已经有其它线程获取了读锁,w!=0并且当前线程不是写锁的持有者,则返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//3、说明当前线程获取了写锁,判断可重入次数
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//4、设置可重入次数
setState(c + acquires);
return true;
}
//5、第一个写线程获取写锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
在代码1中如果当前AQS状态值不为0,则说明当前读锁或写锁已经被其它线程获取,在代码2中如果w==0,说明状态值低16位为0,而AQS状态值不为0,则说明高16为不为0,说明已经有线程获取了读锁,所以直接返回false。
而如果w!=0则说明已经有线程获取了读写锁,然后再判断锁的持有者是不是当前线程。
如果锁的持有者是当前线程则增加可重入次数,同时也要判断可重入次数有没有大于最大可重入次数,如果大於则抛出异常。
如果AQS的状态值为0,则执行代码5,获取读写锁,对于writerShouldBlock方法来说需要做公平以及非公平的实现,非公平的实现为
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}
公平的实现方式为,依然通过hasQueuedPredecessors来判断阻塞队列中是否有前驱节点。
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
2、void unlock()
对于尝试释放锁来说,如果当前线程是锁的持有者,则会让AQS的状态值减1,如果减1之后state为0,则会释放锁,否则仅仅减1.如果当前线程没有持有锁,则会抛出异常IllegalMonitorStateException。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//调用ReentrantReadWriteLock中sync实现的release方法
if (tryRelease(arg)) {
//激活阻塞队列中的一个线程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//判断是否是写锁的持有者调用的unlock
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//获取可重入值,这里没有考虑高16位,因此获取写锁时,读锁的状态肯定为0
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
//如果写锁可重入值为0则释放锁,否则只是简单的更新状态值
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
如上代码中会先通过isHeldExclusively判断当前线程是否是写锁的持有者,如果不是则抛出异常,如果是则获取可重入值,然后判断可重入次数是否为0,为0则释放锁,将锁的持有者设为null,否则只是将状态值减1。
读锁的获取与释放
ReentrantReadWriteLock中的读锁是使用ReadLock来实现的。
1、void lock()
获取读锁,如果当前没有其它线程持有写锁,则当前线程可以获取读锁,AQS的状态值state的高16位的值会增加1,然后返回。否则如果其它线程持有写锁,则当前线程会阻塞。
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
在如上代码中,读锁lock方法调用了AQS的acquireShared方法,在其内部调用了ReentrantReadWriteLock的sync重写的tryAcquireShared方法,代码如下:
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. 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.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
//(1)获取当前状态值
Thread current = Thread.currentThread();
int c = getState();
//(2)判断写锁是否被占用
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//(3)获取读锁计数
int r = sharedCount(c);
//(4)尝试获取锁,多个线程只有一个会成功,不成功的进入fullTryAcquireShared进行重试
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//(5)第一个线程获取读锁
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//(6)获取当前线程是第一个获取读锁的线程
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//(7)记录最后一个获取读锁的线程或记录其它线程读锁的可重入数
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;
}
//(8)类似tryAcquireShared,但是是自旋获取
return fullTryAcquireShared(current);
}
如上代码中,首先获取当前AQS的状态值,然后代码(2)查看其它线程是否获取到了写锁,如果是则直接返回-1,而后调用AQS的doAcquireShared方法把线程放入阻塞队列。
如果当前要获取读锁的线程已经获取了写锁,则也可以获取读锁,但是要注意当一个线程先获取了写锁,然后获取了读锁处理事情完毕后,要记得把读锁和写锁都释放掉,不能只释放写锁。
否则执行代码(3),得到获取到的读锁的个数,到这里说明目前没有线程到写锁,但是可能有线程持有读锁,然后执行代码(4)。其中非公平锁的readerShouldBlock实现代码如下:
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
如上代码的作用是,如果队列里面存在一个元素,则判断元素是不是正在尝试获取写锁,如果不是,则当前线程判断当前获取读锁的线程是否达到了最大值。最后执行CAS操作将AQS的状态值的高16位减1.
代码(5)(6)记录第一个获取到读锁的线程,并统计该线程获取读锁的可重入数,代码(7)使用cachedHoldCounter记录最后一个获取到读锁的线程和该线程获取读锁的可重入数,readHolds记录当前线程获取到读锁的可重入数。
如果readerShouldBlock返回true,则说明有线程正在获取写锁,所以执行代码(8),fullTryAcquireShare的代码与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 = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
2、void unlock()
public void unlock() {
sync.releaseShared(1);
}
如上代码中具体释放锁的操作是委托给Syncll类来做的,sync.releaseShared方法代码如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
//循环直到自己的读计数为-1,CAS更新成功
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;
}
}
如上代码中,在无线循环里,首先获取当前AQS状态值并将其保存到变量c中,然后变量c被减去一个读计数单位后使用CAS操作更新AQS的状态值,如果更新成功则查看当前AQS的状态值是否为0,为0则说明当前已经没有读线程占用读锁,则tryReleaseShared返回true,然后调用doReleaseShared方法释放一个由于获取写锁而被阻塞的线程,如果当前AQS的状态值不为0,则说明当前还有其它线程持有读锁,所以tryReleaseShared返回false。如果tryReleaseShared中的CAS更新AQS状态值失败,则自旋重试直到成功。
新增了读锁的部门,周末两天陪着女朋友去了海边晒了一天的太阳,又玩了一天休息了一天,我是懒散的汤姆,最后的最后给你们吃一波狗粮吧!