一、:ReentrantReadWriteLock
在并发场景中,由于需要解决线程安全问题,独占式锁的使用率非常高,常用的有关键字synchronized或者concurrents包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。
而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。
因此,针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。ReentrantReadWriteLock允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
ReentrantReadWriteLock实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样而已,它的读写锁其实就是两个类:ReadLock、WriteLock,这两个类都是lock实现。在分析WirteLock和ReadLock的互斥性时可以按照WriteLock与WriteLock之间、WriteLock与ReadLock之间以及ReadLock与ReadLock之间的相互影响情况进行分析。
在ReentrantLock中使用了一个int类型的state来表示同步状态,该值表示锁被一个线程重复获取的次数。但是读写锁ReentrantReadWriteLock内部维护着两个配对锁,需要用一个变量维护多种状态。所以读写锁采用“按位切割使用”的方式来维护这个变量,将其切分为两部分,高16位表示读,低16位表示写。
分割之后,读写锁通过位运算迅速确定读锁和写锁的状态。假如当前同步状态为S,那么写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于S >>> 16(无符号补0右移16位)。代码如下:
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
①读写锁的特性
●公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平锁优于公平锁。
●重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时写锁也能够获取读锁。
●锁降级:遵循先获取写锁,再获取读锁,最后释放写锁的次序,写锁能够降级成为读锁。
从上面可以看出,要想能够彻底的理解读写锁必须去理解这样几个问题:
●读写锁是怎样实现分别记录读、写状态的?
●写锁是怎样获取和释放的?
●读锁是怎样获取和释放的?
二、:WirteLock
①获取写锁
这里依然与AQS有很深的联系,下面直接来到核心的tryAcquire()方法:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 获取当前锁的同步状态
int c = getState();
// 获取写锁状态
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// 这里(c != 0 && w == 0)说明有读锁
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 超出锁最大重入次数
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
// 判断是否需要阻塞(公平性)
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
// 设置获取锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
代码主要逻辑为:当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。
这里判断读锁是否存在,是因为要确保写锁的操作对读锁是可见的。如果在存在读锁的情况下允许获取写锁,那么那些已经获取读锁的其他线程可能就无法感知当前写线程的操作。因此只有等读锁完全释放后,写锁才能够被当前线程所获取,一旦写锁获取了,所有其他读、写线程均会被阻塞。
②释放写锁
这里的核心逻辑在tryRelease()方法中:
protected final boolean tryRelease(int releases) {
// 判断释放的线程是否为锁的持有线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 释放后试算
int nextc = getState() - releases;
// 判断写锁的重入次数是否为0
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
写锁释放的整个过程和ReentrantLock相似,每次释放均是减少写状态,当写状态为0时表示 写锁已经完全释放了,从而等待的其他线程可以继续访问读写锁,获取同步状态,同时此次写线程的修改对后续的线程可见。
三、:ReadLock
①获取读锁
读锁是共享的,因此直接来到tryAcquireShared()方法:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果存在写锁,且锁的持有线程不是当前线程(锁降级)
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
// 获取读锁状态
int r = sharedCount(c);
// 判断是否需要阻塞(公平性)、超出锁最大重入次数和设置读锁状态
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 如果是第1次获取锁,则更新firstReader和firstReaderHoldCount
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 如果想要获取锁的线程是第1个获取锁的线程,则将firstReaderHoldCount + 1
} else if (firstReader == current) {
firstReaderHoldCount++;
// HoldCounter处理
} 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);
}
读锁获取的过程如下:
●因为存在锁降级情况,如果存在写锁且锁的持有线程不是当前线程则直接返回失败。
●依据公平性原则,判断读锁是否需要阻塞。判断读锁持有线程数小于最大值且设置锁状态成功,执行后续代码,并返回1。如果不满足条件,执行fullTryAcquireShared()。
1、fullTryAcquireShared()方法
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
// 锁降级
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
// 读锁需要阻塞的情况(公平性)
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
// 列头为当前线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
// HoldCounter的处理
} else {
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");
// CAS设置读锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 如果是第1次获取锁,则更新firstReader和firstReaderHoldCount
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 如果想要获取锁的线程是第1个获取锁的线程,则将firstReaderHoldCount + 1
} else if (firstReader == current) {
firstReaderHoldCount++;
// HoldCounter的处理
} 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尝试获取锁,并返回1。
②释放读锁
与获取同理,直接看tryReleaseShared()方法。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 如果想要释放锁的线程为第一个获取锁的线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
// 如果仅获取了一次,则需要将firstReader设置为null,否则firstReaderHoldCount - 1
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
// 获取HoldCounter对象,并更新 当前线程 获取锁的信息
} 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;
}
// 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;
}
}
根据当前线程是否是持有锁的线程,分别处理。最终更新同步状态。
③HoldCounter相关说明
在读锁获取锁和释放锁的过程中,可以看到一个经常出现的变量rh(HoldCounter),该变量在读锁中有着非常重要的作用。
可以看到,读锁的内在机制其实就是共享锁,而HoldCounter相当于一个计数器。一次共享锁的操作就相当于对该计数器的操作。获取共享锁,则该计数器 + 1;释放共享锁,则该计数器 – 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。
所以HoldCounter的作用就是当前线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其他线程锁就会抛出异常。我们先看HoldCounter的定义:
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
这里可以看到HoldCounter包含了线程id。如果要将一个对象和线程绑定仅仅有线程id是不够的,那么HoldCounter是如何做到的呢?答案就是应用了ThreadLocal:
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
通过上面代码HoldCounter就可以与线程进行绑定了。其中,HoldCounter是绑定线程上的一个计数器,而ThreadLocalHoldCounter则是线程绑定的ThreadLocal。
从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。
这里根据注释可以看到一个特殊点:HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。
看完了HoldCounter的含义,下面再看几个涉及到HoldCounter的变量:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
// 获取读锁的第一个线程
private transient Thread firstReader = null;
// 获取读锁的第一个线程的计数器
private transient int firstReaderHoldCount;
// 最后一个获取到读锁的线程的计数器(每当有新的线程获取到读锁,这个变量都会更新)
private transient HoldCounter cachedHoldCounter;
private transient ThreadLocalHoldCounter readHolds;
}
这里引入firstReader、firstReaderHoldCount是为了提高效率。firstReader是不会放入到readHolds中的,如果读锁仅有一个的情况下就会避免查找readHolds。
回头再看fullTryAcquireShared()方法中的使用场景:
// 非firstReader计数
// 若rh为null,先从缓存值中取
if (rh == null)
rh = cachedHoldCounter;
// 缓存也为null 或者 rh中记录的线程id不是当前线程id 时,需要重新获取rh
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
// 如果计数为0,将rh加入readHolds
else if (rh.count == 0)
readHolds.set(rh);
// 计数 + 1
rh.count++;
cachedHoldCounter = rh;
④锁降级
锁降级:写锁是可以降级为读锁的,但是需要遵循先获取写锁、获取读锁再释放写锁的次序。锁降级是为了提高获取锁的效率。
如果在获取读锁的过程中写锁被持有了,JUC并没有让所有线程傻傻等待,而是判断如果获取读锁的线程是刚好是持有写锁的线程,那么当前线程就可以降级获取写锁,否则就会死锁了。(当持有写锁的线程想获取读锁,但却无法降级,进入了等待队列,肯定会死锁)
如果先释放写锁,再获取读锁,势必引起锁的争抢和线程上下文切换,影响性能。
由此处的逻辑处理,以及前面提到的各种缓存优化,可以看到Doug Lea大神追求性能与正确性到极致的极客精神。这是非常值得我们去学习的。
四、:StampedLock(升级版读写锁)
①StampedLock简介
StampedLock是JDK1.8新增的一个锁,是对读写锁ReentrantReadWriteLock的升级改进。
根据前文我们了解到,在共享数据很大 且 读操作远多于写操作 的情况下,ReentrantReadWriteLock值得一试。但要注意的是,只有当前没有线程持有读锁或者写锁时才能获取到写锁,这可能会导致写线程发生饥饿现象,即读线程太多导致写线程迟迟竞争不到锁而一直处于等待状态。
StampedLock可以解决这个问题,其解决方法是如果在读的过程中发生了写操作,应该重新读而不是直接阻塞写线程。
StampedLock有三种读/写模式:写、读、乐观读。
●写。独占锁,只有当前没有线程持有读锁或者写锁时才能获取到该锁。方法writeLock()返回一个可用于unlockWrite()释放锁的方法的戳记。tryWriteLock()提供不计时和定时的版本。
●读。共享锁,如果当前没有线程持有写锁即可获取该锁,可以由多个线程获取到该锁。方法readLock()返回可用于unlockRead()释放锁的方法的戳记。tryReadLock()也提供不计时和定时的版本。
●乐观读。方法tryOptimisticRead()仅当锁定当前未处于写入模式时,方法才会返回非零戳记。返回戳记后,需要调用validate()方法验证戳记是否可用。也就是看当调用tryOptimisticRead()返回戳记后到到当前时间是否有其他线程持有了写锁,如果有,返回false,否则返回true,这时就可以使用该锁了。
此类还支持有条件地提供三种模式转换的方法。例如,方法tryConvertToWriteLock()试图“升级”模式,如果:①已经处于书写模式;②处于阅读模式并且没有其他读取器;③处于乐观模式且锁定可用。则返回有效的写入标记。这些方法的形式旨在帮助减少在基于重试的设计中发生的一些代码膨胀。
StampedLock是不可重入的。
②实现分析
在进入各种方法之前,先需要了解下StampedLock中的主要内部类和属性:
static final class WNode {
// 前节点
volatile WNode prev;
// 后节点
volatile WNode next;
// 读线程所用的链表(实际是一个栈结果)
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; }
}
WNode是队列中的节点,类似于AQS队列中的节点。可以看到它组成了一个双向链表,内部维护着阻塞的线程。
// 读线程的个数占有低7位
private static final int LG_READERS = 7;
// 读线程个数每次增加的单位
private static final long RUNIT = 1L;
// 写线程个数所在的位置(1000 0000 = 128)
private static final long WBIT = 1L << LG_READERS;
// 读线程个数所在的位置(111 1111 = 127)
private static final long RBITS = WBIT - 1L;
// 最大读线程个数(111 1110 = 126)
private static final long RFULL = RBITS - 1L;
// 读线程个数和写线程个数的掩码(1111 1111 = 255)
private static final long ABITS = RBITS | WBIT;
// 读线程个数的反数,高25位全部为1(1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000 0000 = -128)
private static final long SBITS = ~RBITS; // note overlap with ABITS
// state的初始值(1 0000 0000 = 256)
private static final long ORIGIN = WBIT << 1;
// 头节点
private transient volatile WNode whead;
// 尾节点
private transient volatile WNode wtail;
// 状态值/版本号
private transient volatile long state;
可以看到,这是一个类似于AQS的结构,内部同样维护着一个状态变量state和一个CLH队列。
1、构造方法
public StampedLock() {
state = ORIGIN;
}
构造方法的作用仅为设置初始版本号。
2、writeLock()方法
public long writeLock() {
long s, next; // bypass acquireWrite in fully unlocked case only
// ABITS(1111 1111)、WBITS(1000 0000)
// state与ABITS如果等于0,尝试原子更新state的值加WBITS
// 如果成功则返回更新的值,如果失败调用acquireWrite()方法
return ((((s = state) & ABITS) == 0L &&
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
next : acquireWrite(false, 0L));
}
如果此时state为初始状态,与ABITS与运算后的值为0,所以执行后面的CAS方法,s + WBITS的值为1 1000 0000 = 384。
此时我们大胆猜测:state的高24位存储的是版本号,低8位存储的是是否有加锁,第8位存储的是写锁,低7位存储的是读锁被获取的次数,而且如果只有第8位存储写锁的话,那么写锁只能被获取一次,也就不可能重入了。
ⅰ、acquireWrite()
private long acquireWrite(boolean interruptible, long deadline) {
// node为新增节点,p为尾节点(即将成为node的前置节点)
WNode node = null, p;
// 第一段自旋(入队)
for (int spins = -1;;) { // spin while enqueuing
long m, s, ns;
// 再次尝试获取写锁
if ((m = (s = state) & ABITS) == 0L) {
if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
return ns;
}
else if (spins < 0)
// 如果自旋次数小于0,则计算自旋的次数
// 如果当前有写锁独占且队列无元素,说明快轮到自己了
// 就自旋就行了,如果自旋完了还没轮到自己才入队
// 则自旋次数为SPINS常量
// 否则自旋次数为0
spins = (m == WBIT && wtail == whead) ? SPINS : 0;
else if (spins > 0) {
// 当自旋次数大于0时,当前这次自旋随机减一次自旋次数
if (LockSupport.nextSecondarySeed() >= 0)
--spins;
}
// 如果队列未初始化,新建一个空节点并初始化头节点和尾节点
else if ((p = wtail) == null) { // initialize queue
WNode hd = new WNode(WMODE, null);
if (U.compareAndSwapObject(this, WHEAD, null, hd))
wtail = hd;
}
// 如果新增节点还未初始化,则新建之,并赋值其前置节点为尾节点
else if (node == null)
node = new WNode(WMODE, p);
// 如果尾节点有变化,则更新新增节点的前置节点为新的尾节点
else if (node.prev != p)
node.prev = p;
// 尝试CAS更新新增节点为新的尾节点成功,则退出循环
else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
p.next = node;
break;
}
}
// 第二段自旋(阻塞并等待唤醒)
for (int spins = -1;;) {
// h为头节点,np为新增节点的前置节点,pp为前前置节点,ps为前置节点的状态
WNode h, np, pp; int ps;
// 如果头节点等于前置节点,说明快轮到自己了
if ((h = whead) == p) {
if (spins < 0)
// 初始化自旋次数
spins = HEAD_SPINS;
else if (spins < MAX_HEAD_SPINS)
// 增加自旋次数
spins <<= 1;
// 第三段自旋(不断尝试获取写锁)
for (int k = spins;;) { // spin at head
long s, ns;
if (((s = state) & ABITS) == 0L) {
if (U.compareAndSwapLong(this, STATE, s,
ns = s + WBIT)) {
// 尝试获取写锁成功,将node设置为新头节点并清除其前置节点(help gc)
whead = node;
node.prev = null;
return ns;
}
}
// 随机立减自旋次数,当自旋次数减为0时跳出循环再重试
else if (LockSupport.nextSecondarySeed() >= 0 &&
--k <= 0)
break;
}
}
// 此处很难满足,是用于协助唤醒读节点的
else if (h != null) { // help release stale waiters
WNode c; Thread w;
// 如果头节点的cowait链表(栈)不为空,唤醒里面的所有节点
while ((c = h.cowait) != null) {
if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
}
// 如果头节点没有变化
if (whead == h) {
// 如果尾节点有变化,则更新
if ((np = node.prev) != p) {
if (np != null)
(p = np).next = node; // stale
}
// 如果尾节点状态为0,则更新成WAITING
else if ((ps = p.status) == 0)
U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
// 如果尾节点状态为取消,则把它从链表中删除
else if (ps == CANCELLED) {
if ((pp = p.prev) != null) {
node.prev = pp;
pp.next = node;
}
}
else {
long time; // 0 argument to park means no timeout
// 有超时时间的处理
if (deadline == 0L)
time = 0L;
// 已超时,剔除当前节点
else if ((time = deadline - System.nanoTime()) <= 0L)
return cancelWaiter(node, node, false);
Thread wt = Thread.currentThread();
U.putObject(wt, PARKBLOCKER, this);
// 把node的线程指向当前线程
node.thread = wt;
if (p.status < 0 && (p != h || (state & ABITS) != 0L) &&
whead == h && node.prev == p)
// 阻塞当前线程
U.park(false, time); // emulate LockSupport.park
// 当前节点被唤醒后,清除线程
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
// 如果中断了,取消当前节点
if (interruptible && Thread.interrupted())
return cancelWaiter(node, node, true);
}
}
}
}
此方法的逻辑主要在几个自旋操作中:
第一段自旋(实现入队):
①如果头节点等于尾节点,说明没有其它线程排队,那就多自旋一会,看能不能尝试获取到写锁。
②否则,自旋次数为0,直接让其入队。
第二段自旋(实现阻塞并等待被唤醒) + 第三段自旋(不断尝试获取写锁):
①第三段自旋在第二段自旋内部。
②如果头节点等于前置节点,那就进入第三段自旋,不断尝试获取写锁。
③否则,尝试唤醒头节点中等待着的读线程。
④最后,如果当前线程一直都没有获取到写锁,就阻塞当前线程并等待被唤醒。
3、unlockWrite()方法
public void unlockWrite(long stamp) {
WNode h;
// 检查版本号是否符合条件
if (state != stamp || (stamp & WBIT) == 0L)
throw new IllegalMonitorStateException();
// 这行代码实际有两个作用:1、更新版本号加1;2、释放写锁
// stamp + WBIT实际会把state的第8位置为0,也就相当于释放了写锁;同时会进1,也就是高24位整体加1了
state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
// 如果头节点不为空,并且状态不为0,调用release方法唤醒它的下一个节点
if ((h = whead) != null && h.status != 0)
release(h);
}
ⅰ、release()
private void release(WNode h) {
if (h != null) {
WNode q; Thread w;
// 将其状态改为0
U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
// 如果头节点的下一个节点为空或者其状态为已取消
if ((q = h.next) == null || q.status == CANCELLED) {
// 从尾节点向前遍历找到一个可用的节点
for (WNode t = wtail; t != null && t != h; t = t.prev)
if (t.status <= 0)
q = t;
}
// 唤醒q节点所在的线程
if (q != null && (w = q.thread) != null)
U.unpark(w);
}
}
写锁的释放过程比较简单,大致分为三个部分:
●更改state的值,释放写锁。
●版本号加1。
●唤醒下一个等待着的节点。
4、readLock()方法
public long readLock() {
long s = state, next; // bypass acquireRead on common uncontended case
// 没有写锁占用,并且读锁被获取的次数未达到最大值
// 尝试原子更新读锁被获取的次数加1
// 如果成功直接返回,如果失败调用acquireRead()方法
return ((whead == wtail && (s & ABITS) < RFULL &&
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
next : acquireRead(false, 0L));
}
获取读锁的时候先判断现在有没有其它线程占用着写锁,如果没有的话再判断读锁被获取的次数有没有达到最大,如果没有的话直接尝试获取一次读锁,如果成功了直接返回版本号,如果没成功就调用acquireRead()排队。
ⅰ、acquireRead()
private long acquireRead(boolean interruptible, long deadline) {
// node为新增节点,p为尾节点
WNode node = null, p;
// 第一段自旋(入队)
for (int spins = -1;;) {
// 头节点
WNode h;
// 如果头节点等于尾节点,说明没有排队的线程了,快轮到自己了,直接自旋不断尝试获取读锁
if ((h = whead) == (p = wtail)) {
// 第二段自旋(不断尝试获取读锁)
for (long m, s, ns;;) {
// 尝试获取读锁,如果成功了直接返回版本号(如果读线程个数达到了最大值,会溢出,返回的是0)
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
(m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
return ns;
// 有其它线程先一步获取了写锁
else if (m >= WBIT) {
if (spins > 0) {
// 随机立减自旋次数
if (LockSupport.nextSecondarySeed() >= 0)
--spins;
}
else {
// 如果自旋次数为0了,看是否要跳出循环
if (spins == 0) {
WNode nh = whead, np = wtail;
if ((nh == h && np == p) || (h = nh) != (p = np))
break;
}
// 设置自旋次数
spins = SPINS;
}
}
}
}
// 如果尾节点为空,初始化头节点和尾节点
if (p == null) { // initialize queue
WNode hd = new WNode(WMODE, null);
if (U.compareAndSwapObject(this, WHEAD, null, hd))
wtail = hd;
}
// 如果新增节点为空,将其初始化
else if (node == null)
node = new WNode(RMODE, p);
// 如果头节点等于尾节点或者尾节点不是读模式,当前节点入队
else if (h == p || p.mode != RMODE) {
if (node.prev != p)
node.prev = p;
else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
p.next = node;
break;
}
}
// 尾节点为读模式,将当前节点加入到尾节点的cowait中,这是一个栈
else if (!U.compareAndSwapObject(p, WCOWAIT,
node.cowait = p.cowait, node))
node.cowait = null;
else {
// 第三段自旋(阻塞当前线程并等待被唤醒)
for (;;) {
WNode pp, c; Thread w;
// 如果头节点不为空且其cowait不为空,协助唤醒其中等待的读线程
if ((h = whead) != null && (c = h.cowait) != null &&
U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null) // help release
U.unpark(w);
// 如果头节点等待前前置节点或者等于前置节点或者前前置节点为空,说明快轮到自己了
if (h == (pp = p.prev) || h == p || pp == null) {
long m, s, ns;
// 第四段自旋(不断尝试获取锁)
do {
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s,
ns = s + RUNIT) :
(m < WBIT &&
(ns = tryIncReaderOverflow(s)) != 0L))
return ns;
// 只要当前时刻没有其它线程占有写锁就不断尝试
} while (m < WBIT);
}
// 如果头节点未曾改变且前前置节点也未曾改变,阻塞当前线程
if (whead == h && p.prev == pp) {
long time;
// 如果前前置节点为空,或者头节点等于前置节点,或者前置节点已取消,从第一段自旋开始重试
if (pp == null || h == p || p.status > 0) {
node = null; // throw away
break;
}
// 超时检测
if (deadline == 0L)
time = 0L;
// 如果超时了,取消当前节点
else if ((time = deadline - System.nanoTime()) <= 0L)
return cancelWaiter(node, p, false);
Thread wt = Thread.currentThread();
U.putObject(wt, PARKBLOCKER, this);
// 将当前线程设置进node中
node.thread = wt;
// 检测之前的条件未曾改变
if ((h != pp || (state & ABITS) == WBIT) &&
whead == h && p.prev == pp)
// 阻塞当前线程并等待被唤醒
U.park(false, time);
// 唤醒之后清除线程
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
// 如果中断了,取消当前节点
if (interruptible && Thread.interrupted())
return cancelWaiter(node, p, true);
}
}
}
}
// 只有第一个读线程会走到下面的for循环处,参考上面第一段自旋中有一个break,当第一个读线程入队的时候break出来的
// 第五段自旋
for (int spins = -1;;) {
WNode h, np, pp; int ps;
// 如果头节点等于尾节点,说明快轮到自己了,不断尝试获取读锁
if ((h = whead) == p) {
// 设置自旋次数
if (spins < 0)
spins = HEAD_SPINS;
else if (spins < MAX_HEAD_SPINS)
spins <<= 1;
// 第六段自旋(不断尝试获取读锁)
for (int k = spins;;) { // spin at head
long m, s, ns;
// 不断尝试获取读锁
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
(m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) {
// 获取到了读锁
WNode c; Thread w;
whead = node;
node.prev = null;
// 唤醒当前节点中所有等待着的读线程
// 因为当前节点是第一个读节点,所以它是在队列中的,其它读节点都是挂这个节点的cowait栈中的
while ((c = node.cowait) != null) {
if (U.compareAndSwapObject(node, WCOWAIT,
c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
// 返回版本号
return ns;
}
// 如果当前有其它线程占有着写锁,并且没有自旋次数了,跳出当前循环
else if (m >= WBIT &&
LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
break;
}
}
// 如果头节点不等待尾节点且不为空且其为读模式,协助唤醒里面的读线程
else if (h != null) {
WNode c; Thread w;
while ((c = h.cowait) != null) {
if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
}
// 如果头节点未曾变化
if (whead == h) {
// 更新前置节点及其状态等
if ((np = node.prev) != p) {
if (np != null)
(p = np).next = node; // stale
}
else if ((ps = p.status) == 0)
U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
else if (ps == CANCELLED) {
if ((pp = p.prev) != null) {
node.prev = pp;
pp.next = node;
}
}
// 第一个读节点即将进入阻塞
else {
long time;
// 超时设置
if (deadline == 0L)
time = 0L;
else if ((time = deadline - System.nanoTime()) <= 0L)
// 如果超时了取消当前节点
return cancelWaiter(node, node, false);
Thread wt = Thread.currentThread();
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
if (p.status < 0 &&
(p != h || (state & ABITS) == WBIT) &&
whead == h && node.prev == p)
// 阻塞第一个读节点并等待被唤醒
U.park(false, time);
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
if (interruptible && Thread.interrupted())
return cancelWaiter(node, node, true);
}
}
}
}
这里面一共有六段自旋操作,大致梳理流程如下:
●读节点进来都是先判断是头节点如果等于尾节点,说明快轮到自己了,就不断地尝试获取读锁,如果成功了就返回。
●如果头节点不等于尾节点,这里就会让当前节点入队,这里入队又分成了两种。
①一种是首个读节点入队,它是会排队到整个队列的尾部,然后跳出第一段自旋。
②另一种是非第一个读节点入队,它是进入到首个读节点的cowait栈中,所以更确切地说应该是入栈。
●不管是入队还入栈后,都会再次检测头节点是不是等于尾节点了,如果相等,则会再次不断尝试获取读锁。
●如果头节点不等于尾节点,那么才会真正地阻塞当前线程并等待被唤醒。
●上面说的首个读节点其实是连续的读线程中的首个,如果是两个读线程中间夹了一个写线程,还是老老实实的排队。
5、unlockRead()方法
public void unlockRead(long stamp) {
long s, m; WNode h;
for (;;) {
// 检查版本号
if (((s = state) & SBITS) != (stamp & SBITS) ||
(stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
throw new IllegalMonitorStateException();
// 读线程个数正常(未溢出)
if (m < RFULL) {
// 释放一次读锁
if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) {
// 如果读锁全部都释放了,且头节点不为空且状态不为0,唤醒它的下一个节点
if (m == RUNIT && (h = whead) != null && h.status != 0)
release(h);
break;
}
}
// 读线程个数溢出检测
else if (tryDecReaderOverflow(s) != 0L)
break;
}
}
读锁释放的release()方法同上。释放整体过程为:将state的低7位减1,当减为0的时候说明完全释放了读锁,就唤醒下一个排队的线程。
6、tryOptimisticRead()方法
public long tryOptimisticRead() {
long s;
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
如果没有写锁,就返回state的高25位,这里把写所在位置一起返回了,是为了后面检测数据有没有被写过。
7、validate()方法
public boolean validate(long stamp) {
// 强制加入内存屏障,刷新数据
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
检测两者的版本号是否一致,使用SBITS与操作保证不受读操作的影响。
③总结
●StampedLock也是一种读写锁,但不是基于AQS实现的。
●StampedLock相较于ReentrantReadWriteLock多了一种乐观读的模式,以及读锁转化为写锁的方法。
●StampedLock的state存储的是版本号,确切地说是高24位存储的是版本号,写锁的释放会增加其版本号,读锁不会。
●StampedLock的低7位存储的读锁被获取的次数,第8位存储的是写锁被获取的次数。
●StampedLock不是可重入锁,因为只有第8位标识写锁被获取了,并不能重复获取。
●StampedLock中获取锁的过程使用了大量的自旋操作,对于短任务的执行会比较高效,长任务的执行会浪费大量CPU。
●StampedLock不能实现条件锁。
StampedLock VS ReentrantReadWriteLock
●两者都有获取读锁、获取写锁、释放读锁、释放写锁的方法。
●两者的结构基本类似,都是使用state + CLH队列。
●StampedLock的state分成三段,高24位存储版本号、低7位存储读锁被获取的次数、第8位存储写锁被获取的次数。ReentrantReadWriteLock的state分成两段,高16位存储读锁被获取的次数,低16位存储写锁被获取的次数。
●StampedLock的CLH队列可以看成是变异的CLH队列,连续的读线程只有首个节点存储在队列中,其它的节点存储的首个节点的cowait栈中。ReentrantReadWriteLock的CLH队列是正常的CLH队列,所有的节点都在这个队列中。
●StampedLock获取锁的过程中有判断首尾节点是否相同,即是不是快轮到自己了,如果是则不断自旋,所以适合执行短任务。ReentrantReadWriteLock获取锁的过程中非公平模式下会做有限次尝试。
●StampedLock只有非公平模式,一上来就尝试获取锁。
●StampedLock唤醒读锁是一次性唤醒连续的读锁的,而且其它线程还会协助唤醒。ReentrantReadWriteLock是一个接着一个地唤醒的。
●StampedLock有乐观读的模式,乐观读的实现是通过判断state的高25位是否有变化来实现的。
●StampedLock各种模式可以互转,类似tryConvertToXxx()方法。
●StampedLock写锁不可重入,ReentrantReadWriteLock写锁可重入。
●StampedLock无法实现条件锁,ReentrantReadWriteLock可以实现条件锁。
系列文章传送门:
JUC探险-1、初识概貌
JUC探险-2、synchronized
JUC探险-3、volatile
JUC探险-4、final
JUC探险-5、原子类
JUC探险-6、Lock & AQS
JUC探险-7、ReentrantLock
JUC探险-8、ReentrantReadWriteLock
JUC探险-9、Condition
JUC探险-10、常见工具、数据结构
JUC探险-11、ConcurrentHashMap
JUC探险-12、CopyOnWriteArrayList
JUC探险-13、ConcurrentLinkedQueue
JUC探险-14、ConcurrentSkipListMap
JUC探险-15、BlockingQueue
JUC探险-16、ThreadLocal
JUC探险-17、线程池