和ReentrantLock一样,公平/非公平,可重入等概念可以看之前写过的这篇:
java并发编程ReentrantLock类和可重入锁概念,公平/非公平锁区别,可重入抛异常是否会释放锁
1.锁降级:写线程获取写入锁后可以获取读取锁, 然后释放写入锁, 这样就从写入锁变成了读取锁, 从而实现锁降级的特征
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 lock
rwl.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 lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
锁降级以后, 写锁并不会直接降成读锁, 不会随着读锁的释放而释放, 因此需要显式地释放写锁
锁降级的应用场景: 对于数据比较敏感, 需要在对数据修改以后, 获取到修改后的值, 并进行接下来的其它操作
2.用来提高某些集合的并发性能。当集合比较大,并且读比写频繁时,可以使用该类。下面是TreeMap使用ReentrantReadWriteLock进行封装成并发性能提高的一个例子:
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(); }
}
}
上面这两个例子源码里的都有,可以自己去看
ReentrantReadWriteLock的锁策略有两种,分为公平策略和非公平策略,区别在于随机和顺序获取,非公平吞吐量大,默认非公平。
每个线程中的 R(0)W(0)表示当前线程占用了多少读写锁。
ReentrantReadWriteLock有两个构造方法,和ReentrantLock一样,构造公平/非公平锁,默认非公平
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
Sync继承了aqs,FairSync/NofairSync继承Sync
FairSync实现
两个方法读写block都表示当有别的线程也在尝试获取锁时,是否应该阻塞。对于公平锁,hasQueuedPredecessors()方法表示前面是否有等待线程。一旦前面有等待线程,那么为了遵循公平,当前线程也就应该被挂起。
NonfairSync实现
非公平模式下,写block直接返回false,说明不需要阻塞;而读block调用的方法表示:如果当前有一个写线程正在写,那么该读线程应该阻塞。
继承AQS的类都需要使用state变量代表某种资源,ReentrantReadWriteLock中的state代表了读锁的数量和写锁的持有与否,整个结构如下:
可以看到state的高16位代表读锁的个数;低16位代表写锁的状态。
读锁获取
使用的是aqs的共享模式
当tryAcquireShared
方法小于0时,那么会执行doAcquireShared
方法将该线程加入到等待队列中。
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.
*/
Thread current = Thread.currentThread();
int c = getState();
//如果当前有写线程并且本线程不是写线程,不符合重入,失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//得到读锁的个数
int r = sharedCount(c);
//如果读不应该阻塞并且读锁的个数小于最大值65535,并且可以成功更新状态值,成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//如果当前读锁为0
if (r == 0) {
//第一个读线程就是当前线程
firstReader = current;
firstReaderHoldCount = 1;
}
//如果当前线程重入了,记录firstReaderHoldCount
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);
}
代码分为三步:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//一旦有别的线程获得了写锁,返回-1,失败
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
//如果读线程需要阻塞
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;
}
}
}
在上面可以看到多次调用了readerShouldBlock
方法,对于公平锁,只要队列中有线程在等待,那么将会返回true,也就意味着读线程需要阻塞;对于非公平锁,如果当前有线程获取了写锁,则返回true。一旦不阻塞,那么读线程将会有机会获得读锁。
写锁获取
写锁使用的是AQS的独占模式。首先尝试获取锁,如果获取失败,那么将会把该线程加入到等待队列中。
Sync实现了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.
*/
//得到调用lock方法的当前线程
Thread current = Thread.currentThread();
int c = getState();
//得到写锁的个数
int w = exclusiveCount(c);
//如果当前有写锁或者读锁
if (c != 0) {
// 如果写锁为0或者当前线程不是独占线程(不符合重入),返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//如果写锁的个数超过了最大值,抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 写锁重入,返回true
setState(c + acquires);
return true;
}
//如果当前没有写锁或者读锁,如果写线程应该阻塞或者CAS失败,返回false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//否则将当前线程置为获得写锁的线程,返回true
setExclusiveOwnerThread(current);
return true;
}
代码分为三步:
从上面可以看到调用了writerShouldBlock
方法,FairSync的实现是如果等待队列中有等待线程,则返回false,说明公平模式下,只要队列中有线程在等待,那么后来的这个线程也是需要记入队列等待的;NonfairSync中的直接返回的直接是false,说明不需要阻塞。从上面的代码可以得出,当没有锁时,如果使用的非公平模式下的写锁的话,那么返回false,直接通过CAS就可以获得写锁。
根据代码和上面图解总结一下锁获取:
获取锁做的是更改AQS的状态值以及将需要等待的线程放入到队列中;释放锁要做的就是更改AQS的状态值以及唤醒队列中的等待线程来继续获取锁。
调用tryReleaseShared
方法尝试释放锁,如果释放成功,调用doReleaseShared
尝试唤醒下一个节点。
Sync中的tryReleaseShared
方法实现如下:
protected final boolean tryReleaseShared(int unused) {
//得到调用unlock的线程
Thread current = Thread.currentThread();
//如果是第一个获得读锁的线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
}
//否则,是HoldCounter中计数-1
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;
}
//死循环
for (;;) {
int c = getState();
//释放一把读锁
int nextc = c - SHARED_UNIT;
//如果CAS更新状态成功,返回读锁是否等于0;失败的话,则重试
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;
}
}
从上面可以看到,释放锁的第一步是更新firstReader
或HoldCounter
的计数,接下来进入死循环,尝试更新AQS的状态,一旦更新成功,则返回;否则,则重试。
释放读锁对读线程没有影响,但是可能会使等待的写线程解除挂起开始运行。所以,一旦没有锁了,就返回true,否则false;返回true后,那么则需要释放等待队列中的线程,这时读线程和写线程都有可能再获得锁。
写锁释放
调用tryRelease
尝试释放锁,一旦释放成功了,那么如果等待队列中有线程再等待,那么调用unparkSuccessor
将下一个线程解除挂起。
Sync实现tryRelease
方法如下:
写锁释放分为三步:
从上面可以看到,返回true当且只当没有写锁的情况下,还有写锁则返回false。
根据代码和上面图解总结一下锁释放:
ReentrantLockReadWriteLock中的state同时表示写锁和读锁的个数。state的高16位表示读锁的个数,低16位表示写锁的个数。
AQS有两种模式:共享模式和独占模式,读写锁的实现中,读锁使用共享模式;写锁使用独占模式
另外一点需要记住的即使,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。
参考:
轻松掌握java读写锁(ReentrantReadWriteLock)的实现原理
深入理解读写锁—ReadWriteLock源码分析