1. ReentrantReadWriteLock简介
之前我们介绍过ReentrantLock,它是基于AQS同步框架实现的,是一种可重入的独占锁。但是这种锁在读多写少的场景下,效率并不高。因为当多个线程在进行读操作的时候,实际上并不会影响数据的正确性。
因此针对读多写少的场景,java提供了ReentrantReadWriteLock(可重入读写锁)。读写锁允许同一时刻被多个读线程访问,但是当写线程在访问时,其他所有的读线程和写线程都会被阻塞。
ReentrantReadWriteLock是包含读锁和写锁的,从代码中能够看到:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
public ReentrantReadWriteLock() {
this(false); // 默认非公平锁
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
}
读锁和写锁之间是存在一些关系的,具体如下:
- 读锁和写锁之间是互斥关系;(当有线程持有读锁的时候,想尝试获得写锁的线程不能获得。当有线程持有写锁的时候,想尝试获得读锁的线程不能获得。)
- 读锁和读锁之间是共享关系;
- 写锁和写锁之间是互斥关系;
此外,我们从名字就能够知道,ReentrantReadWriteLock是支持重入的。但是它的重入和ReentrantLock的重入存在些不同。ReentrantReadWriteLock中可重入的含义是:
- 如果一个线程获取了读锁,那么它可以再次获取读锁,但是不能获取写锁;
- 如果一个线程获取了写锁,那么它可以获取写锁或读锁;
2. 锁的升降级
上面我们讲到,当一个线程获得了读锁,不能再重入写锁,这其实是涉及到锁的升降级。
ReentrantReadWriteLock不支持锁升级,即同一个线程获取读锁后,直接申请写锁是不能获取成功的。如下测试代码:
public class Test1 {
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
reentrantReadWriteLock.readLock().lock();
System.out.println("get readlock");
reentrantReadWriteLock.writeLock().lock();
System.out.println("get writelock");
}
}
输出结果:
get readlock
可以看到,在获取了读锁之后,尝试获取写锁是不行的。因此ReentrantReadWriteLock是不支持锁升级的。因为可能其他线程同时持有读锁,而读写锁之间是互斥的,锁升级可能会造成冲突。
下面我们再看一下锁降级的测试代码:
public class Test2 {
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
reentrantReadWriteLock.writeLock().lock();
System.out.println("get writelock");
reentrantReadWriteLock.readLock().lock();
System.out.println("get readlock");
}
}
输出结果
get writelock
get readlock
可以看到,在获取了写锁之后,尝试获取读锁是可以的。因此,ReentrantReadWriteLock是支持锁降级的。因为当该线程持有写锁的时候,肯定不会有其他线程同时持有写锁或读锁,锁降级不存在冲突。
3. 公平锁和非公平锁
ReentrantReadWriteLock中有一个Sync的静态内部内,继承自AQS,重写了AQS中需要重写的方法,因为这在公平锁和非公平锁中都是一样的。但是留下了两个抽象的方法等待公平锁和非公平锁的不同实现:
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();
这两个方法的作用是根据公平和非公平,分别判断当前尝试获得读锁和写锁的线程是否有资格去尝试获得锁。返回true就是没资格,返回false就是有资格。归纳如下:
公平模式:
- 无论当前线程请求写锁还是读锁,只要发现此时还有别的线程在同步队列中等待(写锁or读锁),都一律选择让步,没有资格去竞争锁。
非公平模式:
- 请求写锁时,当前线程会选择直接竞争,不会做丝毫的让步
- 请求读锁时,如果发现同步队列队首线程在等待获取写锁,则会让步。不过这是一种启发式算法,因为写线程可能排在其他读线程后面。这种方式是尽可能避免饥饿。
接下来在加锁代码中会用到。
对于非公平锁,具体实现在NonfairSync内部类中:
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
// AQS
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() && // 判断头节点的下一个节点是否尝试获得写锁
s.thread != null;
}
从上面代码中可以看到,在非公平锁中,如果是尝试获取写锁的线程,就肯定有资格,直接返回false。如果是尝试获取读锁的线程,会去判断头节点的下一个节点是否是尝试获得写锁,这样能够避免写锁线程饥饿。
对于公平锁,具体实现在FairSync内部类中:
static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
// AQS
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
从上面代码中可以看到,在公平锁中,不论尝试获取写锁还是读锁,都是调用hasQueuedPredecessors判断同步队列中是否有前驱,如果有前驱,就乖乖去排队。
4. 写锁代码分析
4.1 加锁
写锁中的加锁代码是lock()函数,又会进一步调用AQS中的acquire函数:
// WriteLock
public void lock() {
sync.acquire(1);
}
// AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
接着会调用tryAcquire函数,因为这个函数在AQS中是空方法,所以在ReentrantReadWriteLock.Sync中重写了这个方法,尝试获取锁。如果获取锁失败,就会进入同步队列进行排队。我们接下来看ReentrantReadWriteLock.Sync.tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread(); //当前线程
int c = getState(); // 同步状态值
int w = exclusiveCount(c); // 独占锁的重数
if (c != 0) { // 如果有线程当前持有锁
// w!=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; // 加锁成功
}
if (writerShouldBlock() || // 是否有资格去尝试获得锁
!compareAndSetState(c, c + acquires)) // CAS更新state
return false;
setExclusiveOwnerThread(current); // 更新成功则设置当前线程独占
return true;
}
在这个方法中,比较重要的方法是exclusiveCount,这个方法是用来判断当前持有写锁的重数。方法如下:
static final int SHARED_SHIFT = 16;
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
可以看到,exclusiveCount方法是将state和EXCLUSIVE_MASK进行相与。而EXCLUSIVE_MASK为1左移16为然后减1,即为0X0000FFFF。两者相与之后,取得同步状态的低16位,就是写锁被获取的次数。
而sharedCount方法是将state无符号右移16位,即取同步状态的高16位,表示读锁被获取的次数。具体如下图所示:
我们再回到tryAcquire方法,写锁的加锁逻辑就是:如果当前读锁被其他线程占有,或写锁被其他线程占有,则加锁失败。否则加锁或重入锁成功。
4.2 解锁
解锁是调用unlock方法,又会进一步调用AQS中的release方法:
public void unlock() {
sync.release(1);
}
//AQS
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
在AQS中的tryRelease方法是空方法,需要自定义的同步器进行实现,具体作用就是尝试解锁。如果解锁成功就会继续唤醒后继线程。接下来我们进入到ReentrantReadWriteLock.Sync.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
return free;
}
这个方法就比较简单,和ReentrantLock释放锁的逻辑差不多。唯一的不同是通过exclusiveCount方法来获取独占锁重入次数的方式不同。
4.3 尝试获得锁
尝试获得锁会调用tryLock方法,这个方法和lock方法的区别在于tryLock是去尝试,拿到就返回true,拿不到就返回false。而lock方法拿不到会一直等待。tryLock代码如下:
public boolean tryLock( ) {
return sync.tryWriteLock();
}
tryLock又会继续调用tryWriteLock方法:
final boolean tryWriteLock() {
Thread current = Thread.currentThread(); // 当前线程
int c = getState(); // 同步状态
if (c != 0) { // 如果当前有线程获取到锁
int w = exclusiveCount(c); // 独占锁重入次数
// 如果是有线程持有读锁或者持有写锁的不是当前线程,就返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 如果已经到了最大重入数
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
// CAS更新state
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current); // 设置线程独占
return true;
}
这个方法的逻辑我们看到是和tryAcquire方法差不多,唯一的区别在于tryAcquire方法中有writerShouldBlock去判断是否有资格。
5. 读锁代码分析
5.1 加锁
加锁调用的是lock函数,又会进一步调用AQS中的acquireShared:
public void lock() {
sync.acquireShared(1);
}
// AQS
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
在acquireShared函数中,会调用tryAcquireShared去尝试获得共享锁。如果获取失败,就会调用doAcquireShared去继续尝试获得锁。在AQS中的tryAcquireShared函数是空方法,所以ReentrantReadWriteLock.Sync进行了重写:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread(); // 当前线程
int c = getState(); // 获取state
if (exclusiveCount(c) != 0 && // 如果发现有线程持有独占锁
getExclusiveOwnerThread() != current) // 且不是当前线程持有独占锁
return -1; // 获取读锁失败
int r = sharedCount(c); // 共享锁的重数
if (!readerShouldBlock() && // 是否有资格去获取锁
r < MAX_COUNT && // 是否超过重数
compareAndSetState(c, c + SHARED_UNIT)) { // 重入
if (r == 0) { // 如果本身没有线程持有读锁
firstReader = current; // 设置第一个读线程为当前线程
firstReaderHoldCount = 1; // 第一个读线程持有重数为1
} else if (firstReader == current) { // 如果本身就是当前线程持有
firstReaderHoldCount++; // 更新重数
} else {
HoldCounter rh = cachedHoldCounter; // 获取缓存的读线程重数
// 如果没有设置过,或cache不是当前线程
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
// 完全释放读锁时,会将holdCounter从ThreadLocal移除,这里重新放入
readHolds.set(rh);
rh.count++; // 重入次数增加
}
return 1; // 加锁成功
}
// 解决读锁重入会因为readerShouldBlock方法重入失败的问题
return fullTryAcquireShared(current);
}
在这个方法中,我们可以看到,当有线程持有写锁的时候,读锁肯定是无法进行加锁的。此外在这个函数中,使用到了cachedHoldCounter这个变量,用来保存最近一次加读锁线程的重数。我们来看下相关代码:
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;
private transient HoldCounter cachedHoldCounter;
HoldCounter包含两个成员变量,分别是count和tid,用来记录读锁的重数和线程id。因为sharedCount只能反映所有读锁线程共同的重数,所以需要一个变量来存储每个线程分别持有读锁的重数。所以这里引入了readHolds这个变量,它是ThreadLocalHoldCounter,是我们之前讲过的ThreadLocal类型的,相当于每个线程都会拥有各自的HoldCounter类型变量,保存了各自的读锁加锁重数,正符合我们的要求。
而cachedHoldCounter相当于是一个缓存,用来记录最近一次加读锁线程的重数。因为每次去readHolds是需要消耗时间的,通过这个缓存可以减少一定量的时间。
我们再回到tryAcquireShared方法,他的逻辑就是:判断是否有线程持有写锁,如果有的话就加锁失败。如果没有,就去尝试获得锁。但是因为readerShouldBlock会导读线程没办法重入,所以就会进入fullTryAcquireShared去解决这个问题:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState(); // 获取state
// 如果有线程持有写锁,就加锁失败
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) { // 如果没有资格去获得锁
// Make sure we're not acquiring read lock reentrantly
// 如果当前线程是第一个线程,那么在tryAcquiredShared中肯定已经重入了
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else { // 如果第一个读线程不是当前线程,在tryAcquireShared无法重入
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");
// 和tryAcquireShared方法实现一样
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;
}
}
}
能进入到fullTryAcquireShared只有两种可能:CAS失败、非第一个读线程重入失败。可以看到fullTryAcquireShared的实现逻辑和tryAcquiredShared是基本相同的,除了没有readerShouldBlock函数。
5.2 解锁
解锁方法会调用unlock函数,又会进一步调用AQS中的releaseShared方法:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
releaseShared中的tryReleaseShared函数就是尝试释放锁,然后唤醒后继线程。AQS中的tryReleaseShared是一个空函数,所以就会调用ReentrantReadWriteLock.Sync的tryReleaseShared函数:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread(); // 当前线程
if (firstReader == current) { // 如果第一个读线程是当前线程
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1) // 读锁重入数为1
firstReader = null; // 释放
else
firstReaderHoldCount--; // 重入数-1
} else { // 如果第一个读线程不是当前线程
HoldCounter rh = cachedHoldCounter; // 获取缓存
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count; // 当前线程持有读锁的重数
if (count <= 1) { // 如果重数为1
readHolds.remove(); // readHolds中删除
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count; // 读锁重数-1
}
for (;;) {
int c = getState(); // 获取state
int nextc = c - SHARED_UNIT; // state-1
if (compareAndSetState(c, nextc)) // CAS更新state值
// 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; // 如果完全释放,就返回true
}
}
从tryReleaseShared方法中可以看到,分为两部分:更新readHolds、更新state。因为释放锁,不仅仅会减少当前线程的读锁重数(readHolds),也要减少全局读锁重数(state)。
5.3 尝试加锁
tryLock方法和之前说的一样,只会进行一次尝试,成功就返回true,失败就返回false:
public boolean tryLock() {
return sync.tryReadLock();
}
接下来进一步调用tryReadLock方法:
final boolean tryReadLock() {
Thread current = Thread.currentThread(); // 当前线程
for (;;) {
int c = getState(); // 获取state
// 如果存在独占锁,且独占锁不是当前线程,加读锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
// 读锁的总重数
int r = sharedCount(c);
if (r == MAX_COUNT) // 超过加锁最大重数
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { // CAS更新state
if (r == 0) { // 如果之前没有人持有读锁
firstReader = current; // 设置第一个读线程为当前线程
firstReaderHoldCount = 1; // 读锁重数为1
} else if (firstReader == current) { // 如果当前线程就是第一个持有读锁的线程
firstReaderHoldCount++; // 第一个线程读锁重数+1
} else {
// 更新readHolds
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++; // 读锁持有重数+1
}
return true; // 加锁成功
}
}
}
这个方法的基本逻辑和tryAcquiredShared是差不多的,只是少了readerShouldBlock
参考文章:
全网最详细的ReentrantReadWriteLock源码剖析(万字长文)
深入理解读写锁ReentrantReadWriteLock