JDK8的一种新的读写锁StampedLock
JDK8新增一种新的读写锁StampedLock。一个最重要的功能改进就是读写锁中解决写线程饥饿的问题。
StampedLock与ReentrantReadWriteLock
以StampedLock与ReentrantReadWriteLock两者比较为线索介绍StampedLock锁。关于ReentrantReadWriteLock锁参考文档ReentrantReadWriteLock源码解析。
StampedLock不基于AQS实现
之前包括ReentrantReadWriteLock,ReentrantLock和信号量等同步工具,都是基于AQS同步框架实现的。而在StampedLock中摒弃了AQS框架,为StampedLock实现提供了更多的灵活性。
StampedLock增加乐观读锁机制
先获取记录下当前锁的版本号stamp,执行读取操作后,要验证这个版本号是否改变,如果没有改变继续执行接下来的逻辑。乐观读锁机制基于在系统中大多数时间线程并发竞争不严重,绝大多数读操作都可以在没有竞争的情况下完成的论断。
//乐观读锁
stamp = lock.tryOptimisticRead();
//do some reading
if (lock.validate(stamp)) {
//do somethinng
}
实际中,乐观读的实现是通过判断state的高25位是否有变化来实现的,获取乐观读锁也仅仅是返回当前锁的版本号。
public long tryOptimisticRead() {
long s;
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
StampedLock锁的状态和版本号
基于AQS实现的ReentrantReadWriteLock,高16位存储读锁被获取的次数,低16位存储写锁被获取的次数。
而摒弃了AQS的StampedLock,自身维护了一个状态变量state。
private transient volatile long state;
StampedLock的状态变量state被分成3段:
- 高24位存储版本号,只有写锁增加其版本号,而读锁不会增加其版本号;
- 低7位存储读锁被获取的次数;
- 第8位存储写锁被获取的次数,因为只有一位用于表示写锁,所以StampedLock不是可重入锁。
关于状态变量state操作的变量设置:
private static final int LG_READERS = 7; //读线程的个数占有低7位
// Values for lock state and stamp operations
private static final long RUNIT = 1L; //读线程个数每次增加的单位
private static final long WBIT = 1L << LG_READERS;//写线程个数所在的位置 1000 0000
private static final long RBITS = WBIT - 1L;//读线程个数的掩码 111 1111
private static final long RFULL = RBITS - 1L;//最大读线程个数
private static final long ABITS = RBITS | WBIT;//读线程个数和写线程个数的掩码 1111 1111
// Initial value for lock state; avoid failure value zero
//state的初始值。 1 0000 0000,也就是高24位最后一位为1,版本号初始值为1 0000 0000。锁获取失败返回版本号0。
private static final long ORIGIN = WBIT << 1;
StampedLock自旋
如下代码片段,可以看到StampedLock锁获取时存在大量自旋逻辑(for循环)。自旋是一种锁优化技术,在并发程序中大多数的锁持有时间很短暂,通过自旋可以避免线程被阻塞和唤醒产生的开销。
自旋技术对于系统中持有锁时间短暂的任务比较高效,但是对于持有锁时间长的任务是对CPU的浪费。
private long acquireWrite(boolean interruptible, long deadline) {
for (int spins = -1;;) { // spin while enqueuing
....省略代码逻辑....
}
for (int spins = -1;;) {
....省略代码逻辑....
for (int k = spins;;) { // spin at head
....省略代码逻辑....
}
private long acquireRead(boolean interruptible, long deadline) {
for (int spins = -1;;) {
....省略代码逻辑....
for (long m, s, ns;;) {
....省略代码逻辑....
}
....省略代码逻辑....
for (;;) {
....省略代码逻辑....
for (int spins = -1;;) {
for (int k = spins;;) { // spin at head
....省略代码逻辑....
}
StampedLock的CLH队列
StampedLock的CLH队列是一个经过改良的队列,在ReentrantReadWriteLock的等待队列中每个线程节点是依次排队,然后责任链设计模式依次唤醒,这样就可能导致读线程全部唤醒,而写线程处于饥饿状态。StampedLock的等待队列,连续的读线程只有首个节点存储在队列中,其它的节点存储在首个节点的cowait队列中。
StampedLock唤醒读锁是一次性唤醒连续的读锁节点。
类WNode是StampedLock等待队列的节点,cowait存放连续的读线程。
static final class WNode {
volatile WNode prev;
volatile WNode next;
//cowait存放连续的读线程
volatile WNode cowait; // list of linked readers
volatile Thread thread; //non-null while possibly parked
volatile int status; // 0, WAITING, or CANCELLED
final int mode; // RMODE or WMODE
WNode(int m, WNode p) { mode = m; prev = p; }
}
如果两个读节点之间有一个写节点,那么这两个读节点就不是连续的,会分别排队。正是因为这样的机制,会按照到来顺序让先到的写线程先于它后面的读线程执行。
StampedLock只有非公平模式,线程到来就会尝试获取锁。