前两篇我们分析了AQS的独占锁和共享锁的实现原理,本篇文章将继续分析AQS的实现者ReentrantReadWriteLock的实现原理!
读写锁维护了一对相关的锁,即读锁和写锁,读锁是共享锁,允许多个线程同时访问资源,而写锁是独占锁,任一时刻只允许一个线程占有独占锁。读写锁适用于读多写少的情况。首先,思考以下几个问题!
1.我们知道AQS维护着一个同步状态state,那么读写锁是如何协调读写线程的呢?
2.读写锁如何实现公平性?
实现原理
1.读写状态的控制
AQS 的状态state是32位(int 类型)的,辦成两份,读锁用高16位,表示持有读锁的线程数(sharedCount),写锁用低16位,表示写锁的重入次数 (exclusiveCount)。状态值为 0 表示锁空闲,sharedCount不为 0 表示分配了读锁,exclusiveCount 不为 0 表示分配了写锁,sharedCount和exclusiveCount 一般不会同时不为 0,只有当线程占用了写锁,该线程可以重入获取读锁,反之不成立。
2.公平模式和非公平模式
公平模式:判断同步队列是否有等待线程。一旦前面有等待线程,那么为了遵循公平,当前线程也就应该加入同步队列。
非公平模式:
如果线程想获取写锁,那么写线程不应该阻塞。
如果线程想获取读锁,通常不需要阻塞,除了这样一种情况:当全局处于读锁状态,且等待队列中第一个等待线程想获取写锁,那么当前线程能够获取到读锁的条件为:当前线程获取了写锁,还未释放;当前线程获取了读锁,这一次只是重入读锁而已;其它情况当前线程入队尾。之所以这样处理一方面是为了效率,一方面是为了避免想获取写锁的线程饥饿,老是得不到执行的机会。
例如:线程C请求一个写锁,由于当前其他两个线程拥有读锁,写锁获取失败,线程C入队列(根据规则i),如下所示
AQS初始化会创建一个空的头节点,C入队列,然后会休眠,等待其他线程释放锁唤醒。
此时线程D也来了,线程D想获取一个读锁,上面规则,队列中第一个等待线程C请求的是写锁,为避免写锁迟迟获取不到,并且线程D不是重入获取读锁,所以线程D也入队,如下图所示:
之所以这样处理一方面是为了效率,一方面是为了避免想获取写锁的线程饥饿,老是得不到执行的机会。
继承关系
首先通过一张继承关系图从总体上了解读写锁的实现原理。
首先Sync类继承了AQS,Sync对AQS中的同步状态state一分为二,高十六位表示读锁计数,低十六位表示写锁计数,并重写了四个最重要的方法,tryAcquire和tryRealse方法定义了获取独占锁的相关规则,tryAcquireShared和tryRealseShared方法定义了获取共享锁的相关规则。
ReentrantReadWriteLock通过引用Sync,并将该引用传递给引用ReadLock和WriteLock,这样ReadLock和WriteLock使用的是同一个Sync,通过控制同一个同步状态state来控制读写线程。
源码分析
首先看一下对state同步状态一分为二,读写锁计数。
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
// 由于读锁用高位部分,所以读锁个数加1,其实是状态值加 2^16
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 写锁的可重入的最大次数、读锁允许的最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 写锁的掩码,用于状态的低16位有效值
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; }
}
我们知道读写锁支持多个读线程同时拥有读锁,那么如何控制每个线程重入读锁的次数呢?
abstract static class Sync extends AbstractQueuedSynchronizer {
/**
* 每个线程特定的 read 持有计数。存放在ThreadLocal,不需要是线程安全的。
*/
static final class HoldCounter {
int count = 0;
// 使用id而不是引用是为了避免保留垃圾。注意这是个常量,一旦创建不能更改。
final long tid = Thread.currentThread().getId();
}
/**
* 采用继承是为了重写 initialValue 方法,这样就不用进行这样的处理:
* 如果ThreadLocal没有当前线程的计数,则new一个,再放进ThreadLocal里。
* 可以直接调用 get,ThreadLocal调用get方法时,如果为空会调用initialValue方法。
* */
static final class ThreadLocalHoldCounter
extends ThreadLocal {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
/**
* 保存当前线程重入读锁的次数的容器。在读锁重入次数为 0 时移除。
*/
private transient ThreadLocalHoldCounter readHolds;
/**
* 最近一个成功获取读锁的线程的计数。
* 通常情况下,下一个释放线程是最后一个获取线程。这不是 volatile 的,
* 仅用作借鉴缓存,可以在一定程度上避免访问readHolds中的HoldCounter。
* (因为判断是否是当前线程是通过线程id来比较的)。
*/
private transient HoldCounter cachedHoldCounter;
/**
* firstReader是这样一个特殊线程:它是最后一个把 共享计数 从 0 改为 1 的
* (在锁空闲的时候),而且从那之后还没有释放读锁的(释放了读锁,firstReader会重置为
* null)。如果不存在则为null。
* firstReaderHoldCount 是 firstReader 的重入计数。
*
* firstReader 不能导致保留垃圾,因此在 tryReleaseShared 里设置为null。
* 除非线程异常终止,没有释放读锁。
*
* 作用是在跟踪无竞争的读锁计数时非常便宜。
*
* firstReader及其计数firstReaderHoldCount是不会放入 readHolds 的。
*/
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
Sync() {
readHolds = new ThreadLocalHoldCounter();
// 确保 readHolds 的内存可见性,利用 volatile 内存语义,即禁止重排序。
setState(getState());
}
}
这里有一个疑问,既然readHolds可以保存所有线程的重入计数,咋还使用了firstReader和firstReaderHoldCount单独保存第一个读线程重入计数。cachedHoldCounter保存最后一个读线程的重入计数。
源码多次使用firstReader和cachedHoldCounter来进行重入计数判断,如果不是才使用readHolds先读取在设置重入计数。
比如只有一个读线程获取读锁,那么也就没有必要设置ThreadLocal变量readHolds。这里顺便说一下ThreadLocal的缺点:
1. 容易造成内存泄漏。thread local 是实际是两级以上的hashtable,一旦你还用线程池的话,用的不好可能永远把内存占着。造成java VM上的内存泄漏。
2. 代码的耦合度高,且测试不易。
具体原因的可以上网百度。
写锁的获取
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//1.获取同步状态
int c = getState();
//2.获取写锁计数
int w = exclusiveCount(c);
//3.如果同步状态不等于0
if (c != 0) {
// 4.如果同步状态不等于0,且写锁计数=0,说明读锁计数不等于0
//或者同步状态不等于0,写锁不等于0,并且非重入,那么返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//5.如果写锁计数即将超过最大数,抛出异常(写锁数量超出最大值)
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//6.否则就更新state,重入写锁成功
setState(c + acquires);
return true;
}
//7.如果同步状态为0,队列政策允许,就CAS设置state
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//8.设置state成功,就设置独占锁拥有着owner,获取写锁成功。
setExclusiveOwnerThread(current);
return true;
}
由上面分析可以得出写锁获取的条件:
1.如果存在读线程正在占用读锁,则写锁获取失败。
2.如果没有读线程,但存在写线程正在占用写锁,除非是重入写锁,否则获取失败。
3.如果没有读写线程占用读写锁,如果队列政策允许就获取写锁。
写锁的释放
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//由于写锁是独占锁,所以不用考虑并发安全问题。
int nextc = getState() - releases;
//1.释放写锁后的独占计数是否等于0。
boolean free = exclusiveCount(nextc) == 0;
//2.写锁计数为0,就设置独占线程引用为null
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
相对于写锁的获取,写锁的释放较为简单,因为写锁是独占锁,不用考虑并发安全问题。
读锁的获取
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//1.如果写锁计数不等于0,且非重入,返回-1,获取读锁失败。
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//2.写锁计数等于0,可以获取读锁
//3.读锁计数
int r = sharedCount(c);
//4。此处首先判断队列政策是否允许获取读锁,如果允许在判断读锁计数是否小于最大值,如果小于
//继续通过CAS设置同步状态,设置成功,即表示获取读锁成功。
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
//5.如果读锁计数为0,说明当前线程是新的第一个获取读锁的线程
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//6.如果当前线程是第一个获取读锁的线程,那么firstReaderHoldCount增1
firstReaderHoldCount++;
} else {
//7.否则,先通过缓存线程计数,判断是否是当前线程,避免了访问readHolds
HoldCounter rh = cachedHoldCounter;
//8.如果缓存线程计数为null或者缓存线程计数不是当前线程,那么从readHolds中取。
if (rh == null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
//9.如果缓存线程计数是当前线程,并且count=0,那么需要重新设置readHolds中的缓存线程计数。
//因为当count=0时,缓存线程计数会从readHolds中删除,故需要重新设置
else if (rh.count == 0)
readHolds.set(rh);
//10.count自增。
rh.count++;
}
//11.获取读锁成功,返回1
return 1;
}
//12.否则,获取失败就调用fullTryAcquireShared循环获取读锁。
return fullTryAcquireShared(current);
}
注意:
第5点,如果读锁计数为0,说明当前线程是新的第一个获取读锁的线程。而firstReader和firstReaderHoldCount并没有使用volatile修饰,也没有保证原子性,那么如何保证线程安全的呢?
因为firstReader代表新的第一个将读锁计数从0变为1的线程,有且只有一个,所以不存在并发。有人就会说,可能存在多个线程尝试将读锁计数从0变1,这就是设计者精妙之处,在进入第五处代码之前,是先通过CAS设置了同步状态,故有且只有一个线程能够成功。
注意:firstReader及其计数firstReaderHoldCount是不会放入 readHolds 的。firstReader是这样一个特殊线程:它是最后一个把 共享计数 从 0 改为 1 的而且从那之后还没有释放读锁的。如果不存在则为null。
fullTryAcquireShared是获取读锁的最全版本,用来处理在tryAcquireShared中CAS设置失败,或者重入读未处理。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//1.如果写锁计数不等于0,且非重入,返回-1,获取读锁失败。
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
//2.如果队列政策认为当前线程应该阻塞
if (firstReader == current) {
// 3.如果当前线程是firstReader,那么firstReaderHoldCount > 0,即当前线程还没有释放读锁,
//这次是重入读锁;
} else {
if (rh == null) {
//4.如果不是firstReader,先看看是否是最后线程cachedHoldCounter
rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId()) {
rh = readHolds.get();
//5.如果当前线程也不是最后线程cachedHoldCounter,那么就通过readHolds获取HoldCounter
//6.如果当前线程读锁计数为0,那么就从readHolds移除本线程的HoldCounter
if (rh.count == 0)
readHolds.remove();
}
}
//7.如果当前线程读锁计数为0,那么此次线程非重入读锁,返回-1,获取读锁失败。
if (rh.count == 0)
return -1;
}
}
//8.代码到这,表示可以获取读锁
//9.如果读锁计数已经达到最大值。
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
//10.如果CAS设置同步状态成功,下面步骤就和tryAquiredShared一样了,参考tryAquiredShared
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
注意:第2处,即使readerShouldBlock()返回true,即队列政策认为当前线程应该阻塞,也要判断是否是读锁重入。此处参考最上面的实现原理2的分析图。
读锁的释放
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
//1.如果当前线程是firstReader,那么firstReaderHoldCount > 0;
//2.如果firstReaderHoldCount=1,那么这次释放读锁,就需要设置firstReader=null
if (firstReaderHoldCount == 1)
firstReader = null;
//3.否则firstReader读锁计数自减
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
//4.否则,就先判断当前是否是cachedHoldCounter
if (rh == null || rh.tid != current.getId())
//5.如果不是,就从readHolds中读取
rh = readHolds.get();
int count = rh.count;
//6.如果count=1,那么就需要从readHolds移除当前线程计数
if (count <= 1) {
readHolds.remove();
//7.如果count=0,抛出异常
if (count <= 0)
throw unmatchedUnlockException();
}
//8.读锁计数自减
--rh.count;
}
//9.到此,就处理好了当前线程的读锁计数。下面就循环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;
}
}
至此,读写锁的关键点已经分析完成了,接下来,我们还会分析JUC中其他的并发组件。