前言
读写锁,即ReentrantReadWriteLock
,同一时刻可以允许多个读线程获取锁,但当写线程获取锁后,读线程和其它写线程应该被阻塞。
正文
下面以一个单例缓存的例子来说明ReentrantReadWriteLock
的使用。
public class Cache {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock rLock = rwLock.readLock();
private final Lock wLock = rwLock.writeLock();
private final Map<String, String> map = new HashMap<>();
private static Cache instance = null;
private Cache() {}
public Cache getCacheInstance() {
if (instance == null) {
synchronized (Cache.class) {
if (instance == null) {
instance = new Cache();
}
}
}
return instance;
}
public String getValueByKey(String key) {
rLock.lock();
try {
return map.get(key);
} finally {
rLock.unlock();
}
}
public void addValueByKey(String key, String value) {
wLock.lock();
try {
map.put(key, value);
} finally {
wLock.unlock();
}
}
public void clearCache() {
wLock.lock();
try {
map.clear();
} finally {
wLock.unlock();
}
}
}
根据例子可知,ReentrantReadWriteLock
提供了一对锁:写锁和读锁,并且使用规则如下。
- 当前线程获取读锁时,读锁是否被获取不会影响读锁的获取;
- 当前线程获取读锁时,若写锁未被获取或者写锁被当前线程获取,则允许获取读锁,否则进入等待状态;
- 当前线程获取写锁时,若读锁已经被获取,无论获取读锁的线程是否是当前线程,都进入等待状态;
- 当前线程获取写锁时,若写锁已经被其它线程获取,则进入等待状态。
下面将结合源码对写锁和读锁的获取和释放进行分析。首先看一下ReentrantReadWriteLock
的类图。
ReentrantReadWriteLock
一共有五个内部类,分别为Sync
,FairSync
,NonfairSync
,WriteLock
和ReadLock
,同时可以看到,只有WriteLock
和ReadLock
实现了Lock
接口,因此ReentrantReadWriteLock
的写锁和读锁的获取和释放实际上是由WriteLock
和ReadLock
来完成,所以这里对ReentrantReadWriteLock
的工作原理进行一个简单概括:ReentrantReadWriteLock
的写锁和读锁的获取和释放分别由其内部类WriteLock
和ReadLock
来完成,而WriteLock
和ReadLock
对同步状态的操作又是依赖于ReentrantReadWriteLock
实现的三个自定义同步器Sync
,FairSync
和NonfairSync
。
下面继续分析写锁和读锁的同步状态的设计。通过上面的分析可以知道WriteLock
和ReadLock
依赖同一个自定义同步组件Sync
,因此WriteLock
和ReadLock
对同步状态进行操作时会修改同一个state变量,即需要在同一个整型变量state上维护写锁和读锁的同步状态,而Java
中整型变量一共有32位,所以ReentrantReadWriteLock
将state的高16位表示读锁的同步状态,低16位表示写锁的同步状态。鉴于读写锁同步状态的设计,对读写锁同步状态的运算操作归纳如下。
- 获取写锁同步状态: state & 0x0000FFFF
- 获取读锁同步状态: state >>> 16
- 写锁同步状态加一: state + 1
- 读锁同步状态加一: state + (1 << 16)
理清楚了ReentrantReadWriteLock
的组件之间的关系和读写锁同步状态的设计之后,下面开始分析写锁和读锁的获取和释放。
1. 写锁的获取
WriteLock
的lock()
方法直接调用了AbstractQueuedSynchronizer
的模板方法acquire()
,在acquire()
方法中会调用自定义同步器Sync
重写的tryAcquire()
方法,下面看一下tryAcquire()
方法的实现。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// c表示state
int c = getState();
// w表示写锁同步状态
int w = exclusiveCount(c);
if (c != 0) {
// state不为0,但是写锁同步状态为0,表示读锁已经被获取
// 获取写锁时只要读锁被获取过,就不允许获取写锁
// 因为写锁是独占锁,所以持有写锁的线程不是当前线程也不允许获取写锁
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 执行到这里表示写锁重入
setState(c + acquires);
return true;
}
// 非公平获取锁时writerShouldBlock()返回false
// 公平获取锁时writerShouldBlock()会调用hasQueuedPredecessors()方法
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
上述tryAcquire()
方法中,在获取写锁之前会判断读锁是否被获取以及写锁是否被其它线程获取,任意一个条件满足都不允许当前线程获取写锁。同时如果写锁和读锁均没有被获取,即state为0时,还会调用writerShouldBlock()
方法来实现非公平或公平锁的语义,如果是非公平锁,writerShouldBlock()
方法会返回false,此时当前线程会以CAS方式修改state,修改成功则表示获取读锁成功,如果是公平锁,writerShouldBlock()
方法会调用hasQueuedPredecessors()
方法来判断同步队列中是否已经有正在等待获取锁资源的线程,如果有,则当前线程需要加入同步队列,后续按照等待时间越久越优先获取锁的机制来获取写锁。
2. 写锁的释放
WriteLock
的unlock()
方法直接调用了AbstractQueuedSynchronizer
的模板方法release()
,在release()
方法中会调用自定义同步器Sync
重写的tryRelease()
方法,下面看一下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);
return free;
}
因为写锁支持重入,所以在释放写锁时会对写锁状态进行判断,只有写锁状态为0时,才表示写锁被成功释放掉。
3. 读锁的获取
ReadLock
的lock()
方法直接调用了AbstractQueuedSynchronizer
的模板方法acquireShared()
,在acquireShared()
方法中会调用自定义同步器Sync
重写的tryAcquireShared()
方法,tryAcquireShared()
方法并不完整,其最后会调用fullTryAcquireShared()
方法,该方法的注释说明如下。
获取读锁同步状态的完整版本,能够实现在
tryAcquireShared()
方法中未能实现的CAS设置状态失败重试和读锁重入的功能。
在JDK1.6
中ReentrantReadWriteLock
提供了getReadHoldCount()
方法,该方法用于获取当前线程获取读锁的次数,因为该方法的加入,导致了读锁的获取的逻辑变得更为复杂,下面将结合tryAcquireShared()
和fullTryAcquireShared()
方法的实现,在抛开为实现getReadHoldCount()
方法功能而新增的逻辑的情况下,给出读锁获取的简化实现代码。
final int fullTryAcquireShared(Thread current) {
for (;;) {
// c表示state
int c = getState();
// 如果写锁被获取并且获取写锁的线程不是当前线程,则不允许获取读锁
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 安全的将读锁同步状态加1
if (compareAndSetState(c, c + SHARED_UNIT))
return 1;
}
}
由上述可知,读锁在写锁被获取并且获取写锁的线程不是当前线程的情况下,不允许被获取,以及读锁的同步状态为所有线程获取读锁的次数之和。
4. 读锁的释放
ReadLock
的unlock()
方法直接调用了AbstractQueuedSynchronizer
的模板方法releaseShared()
,在releaseShared()
方法中会调用自定义同步器Sync
重写的tryReleaseShared()
方法,该方法同样在JDK1.6
中加入了较为复杂的逻辑,下面给出其简化实现代码。
protected final boolean tryReleaseShared(int unused) {
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
由上述可知,只有在state为0时,即读锁和写锁均被释放的情况下tryReleaseShared()
方法才会返回true,在官方的注释中给出了这样设计的原因,如下所示。
释放读锁对读线程没有影响,但是当读锁和写锁均被释放的情况下,在同步队列中等待的写线程就有可能去获取写锁。
总结
ReentrantReadWriteLock
的写锁和读锁的获取和释放分别由其内部类WriteLock
和ReadLock
来完成,而WriteLock
和ReadLock
对同步状态的操作又是依赖于ReentrantReadWriteLock
实现的三个自定义同步器Sync
,FairSync
和NonfairSync
。
写锁和读锁,使用规则如下。
- 当前线程获取读锁时,读锁是否被获取不会影响读锁的获取;
- 当前线程获取读锁时,若写锁未被获取或者写锁被当前线程获取,则允许获取读锁,否则进入等待状态;
- 当前线程获取写锁时,若读锁已经被获取,无论获取读锁的线程是否是当前线程,都进入等待状态;
- 当前线程获取写锁时,若写锁已经被其它线程获取,则进入等待状态。