深入理解读写锁ReentrantReadWriteLock
Java并发(7)- 你真的了解 ReentrantReadWriteLock 吗?
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
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); //独占
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {}
与 ReentranReadWriteLock:
相同:使用了同一个关键实现AbstractQueuedSynchronizer
不同的:ReentrantReadWriteLock使用了两个锁分别实现了AQS,而且WriteLock和ReentrantLock一样,使用了独占锁。而ReadLock和Semaphore一样,使用了共享锁。
独占锁通过state变量的0和1两个状态来控制是否有线程占有锁,共享锁通过state变量0或者非0来控制多个线程访问。ReadLock和WriteLock使用了同一个AQS。
WriteLock不仅需要考虑是否有其他写锁占用,同时还要考虑是否有其他读锁,而ReentrantLock只需要考虑自身是否被占用就行了。
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && //尝试获取独占锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //获取失败后排队
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); //获取共享变量state
int w = exclusiveCount(c); //获取写锁数量
if (c != 0) { //有读锁或者写锁
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread()) //写锁为0(证明有读锁),或者持有写锁的线程不为当前线程
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires); //当前线程持有写锁,为重入锁,+acquires即可
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires)) //CAS操作失败,多线程情况下被抢占,获取锁失败。CAS成功则获取锁成功
return false;
setExclusiveOwnerThread(current);
return true;
}
当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。
写锁释放通过重写AQS的tryRelease方法,源码为:
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//1. 同步状态减去写状态
int nextc = getState() - releases;
//2. 当前写状态是否为0,为0则释放写锁
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
//3. 不为0则更新同步状态
setState(nextc);
return free;
}
减少写状态int nextc = getState() - releases;
只需要用当前同步状态直接减去写状态的原因正是写状态是由同步状态的低16位表示的。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current) //写锁不等于0的情况下,验证是否是当前写锁尝试获取读锁
return -1;
int r = sharedCount(c); //获取读锁数量
if (!readerShouldBlock() && //读锁不需要阻塞
r < MAX_COUNT && //读锁小于最大读锁数量
compareAndSetState(c, c + SHARED_UNIT)) { //CAS操作尝试设置获取读锁 也就是高位加1
if (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);
}
在上面的代码中尝试获取读锁的过程和获取写锁的过程也很相似,不同在于读锁只要没有写锁占用并且不超过最大获取数量都可以尝试获取读锁,而写锁不仅需要考虑读锁是否占用,也要考虑写锁是否占用。
上面的代码中firstReader,firstReaderHoldCount以及cachedHoldCounter都是为readHolds(ThreadLocalHoldCounter)服务的,用来记录每个读锁获取线程的获取次数,方便获取当前线程持有锁的次数信息。
在ThreadLocal基础上添加了一个Int变量来统计次数,可以通过他们的实现来理解:
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> { //ThreadLocal变量ß
public HoldCounter initialValue() {
return new HoldCounter();
}
}
static final class HoldCounter {
int count = 0; //当前线程持有锁的次数
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread()); //当前线程ID
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 前面还是为了实现getReadHoldCount等新功能
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;
}
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;
}
}
读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级。
public static void ReentrantReadWriteLockCacheSystem() {
//这里为了实现简单,将缓存大小设置为4。
Map<String, String> cacheMap = new HashMap<>(4);
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
for (int i = 0; i < 20; i++) { //同时开启20个线程访问缓存
final String key = String.valueOf(i % 4);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
//①读取缓存时获取读锁
readWriteLock.readLock().lock();
//获取读锁后通过key获取缓存中的值
String valueStr = cacheMap.get(key);
//缓存值不存在
if (valueStr == null) {
//③释放读锁后再尝试获取写锁
readWriteLock.readLock().unlock();
try {
//④获取写锁来写入不存在的key值,
readWriteLock.writeLock().lock();
valueStr = cacheMap.get(key);
if (valueStr == null) {
valueStr = key + " --- value";
cacheMap.put(key, valueStr); //写入值
System.out.println(Thread.currentThread().getName() + " --------- put " + valueStr);
}
// ⑥锁降级,避免被其他写线程抢占后再次更新值,保证这一次操作的原子性
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + " --------- get new " + valueStr);
} finally {
readWriteLock.writeLock().unlock(); //⑤释放写锁
}
} else {
System.out.println(Thread.currentThread().getName() + " ------ get cache value");
}
} finally {
readWriteLock.readLock().unlock(); //②释放读锁
}
}
}, String.valueOf(i));
thread.start();
}
}
首先线程会尝试去获取数据,需要获取读锁①,如果存在值,则直接读取并释放读锁②。如果不存在值,则首先释放已经获取的读锁③,然后尝试获取写锁④。获取到写锁之后,再次检查值,因为此时可能存在其他写锁已经更新值,这时只需要读取,然后释放写锁⑤。如果还是没有值,则通过其他途径获取值并更新然后获取读锁⑥,这一步锁降级操作是为了直接抢占读锁,避免释放写锁之后再次获取读锁时被其他写线程抢占,这样保证了这一次读取数据的原子性。之后再执行⑤释放写锁和②释放读锁。
执行后输出结果如下,每次执行可能输出不同:
//1 --------- put 1 --- value
//1 --------- get new 1 --- value
//0 --------- put 0 --- value
//0 --------- get new 0 --- value
//9 ------ get cache value
//4 ------ get cache value
//2 --------- put 2 --- value
//2 --------- get new 2 --- value
//11 --------- put 3 --- value
//11 --------- get new 3 --- value
//5 ------ get cache value
//13 ------ get cache value
//6 ------ get cache value
//8 ------ get cache value
//7 ------ get cache value
//3 --------- get new 3 --- value
//10 ------ get cache value
//12 ------ get cache value
//14 ------ get cache value
//15 ------ get cache value
//16 ------ get cache value
//17 ------ get cache value
//18 ------ get cache value
//19 ------ get cache value
ReentrantReadWriteLock会发生写请求饥饿的情况,因为写请求一样会排队,不管是公平锁还是非公平锁,在有读锁的情况下,都不能保证写锁一定能获取到,这样只要读锁一直占用,就会发生写饥饿的情况。
DK8中新增的改进读写锁—StampedLock 可以解决。