Java中的AQS实现了多线程中很重要的两套机制——独占锁和共享锁。在此文中只针对共享锁的源码进行讲解分析。
如果想了解独占锁的朋友可以看我的另外一篇博文:AbstractQueuedSynchronizer(AQS)源码分析——独占锁
共享锁是什么?
简单来说,共享锁可以被多个线程共同持有。在Java中最经典的实现就是ReentrantReadWriteLock(读写锁)。在读写锁中有两个锁对象,一个是读锁,另一个是写锁。
- 读锁属于共享锁,可以让多个线程同时拥有,即可以让多个线程在同一时间进行读操作
- 写锁属于独占锁,在同一时间只能有一个线程进行写操作
换句话说,读写锁可以做到:读读-允许、写写-互斥、读写-互斥、写读-互斥
因为本文只讲解共享锁,所以从ReentrantReadWriteLock中的readerLock(读锁)入手。
对于锁来说,最重要的操作自然是获取、释放。因此在之后的内容都是针对于读锁的获取和释放的源码进行讲解分析。
首先了解AQS中的重要内部类——Node
在此只列出Node中重点属性。需要了解好这些属性的作用,才能更好理解之后的源码分析。
static final class Node {
// 共享模式的标记
static final Node SHARED = new Node();
// 独占模式的标记
static final Node EXCLUSIVE = null;
// waitStatus变量的值,标志着线程被取消
static final int CANCELLED = 1;
// waitStatus变量的值,标志着后继线程(即队列中此节点之后的节点)需要被阻塞.(用于独占锁)
static final int SIGNAL = -1;
// waitStatus变量的值,标志着线程在Condition条件上等待阻塞.(用于Condition的await等待)
static final int CONDITION = -2;
// waitStatus变量的值,标志着下一个acquireShared方法线程应该被允许。(用于共享锁)
static final int PROPAGATE = -3;
// 标记着当前节点的状态,默认状态是0, 小于0的状态都是有特殊作用,大于0的状态表示已取消
volatile int waitStatus;
// prev和next实现一个双向链表
volatile Node prev;
volatile Node next;
// 该节点拥有的线程
volatile Thread thread;
//
Node nextWaiter;
}
在此进一步说明waitStatus属性:
-
当waitStatus = CANCELLED = 1:
表示Node所代表的当前线程已经取消了排队,即放弃获取锁了。
-
当waitStatus = SIGNAL = -1:
它不是表征当前节点的状态,而是当前节点的下一个节点的状态。
(当一个节点的waitStatus被置为SIGNAL,就说明它的下一个节点(即它的后继节点)已经被挂起了(或者马上就要被挂起了),因此在当前节点释放了锁或者放弃获取锁时,如果它的waitStatus属性为SIGNAL,它还要完成一个额外的操作——唤醒它的后继节点。) -
当waitStatus = CONDITION = -2:
表示这个Node在条件队列中,因为等待某个条件而被阻塞。
-
当waitStatus = PROPAGATE = -3:
使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播。
获取锁方法acquireShared()
基于acquireShared()方法有两种实现,一种是公平锁,另一种是非公平锁。因为Java中默认使用的是非公平锁,因此在本文的源码分析都是基于非公平锁。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // 小于0,获取锁失败;大于等于0,获取锁成功
doAcquireShared(arg); // 获取锁失败,继续获取共享锁
}
- 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)) { // 获取读锁不应该阻塞,而且比较更新state操作成功,则尝试获取读锁成功
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} 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); // 获取读锁应该被阻塞,或者多线程下因为有多个读锁进行更新state,比较更新state操作失败
}
- readerShouldBlock()方法判断当前线程获取读锁是否应该阻塞
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null; // 查看等待队列中的第一个等待结点是否是共享结点。若是,则说明获取共享锁不应该阻塞,否则应该阻塞
}
- fullTryAcquireShared()方法采用自旋方式再次尝试获取读锁
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) { // 自旋
int c = getState();
if (exclusiveCount(c) != 0) { // 写锁被占用
if (getExclusiveOwnerThread() != current) // (这个判断很重要!)判断占用写锁的线程不是当前线程,则尝试获取读锁失败
// 试想下这种场景:
// 当前线程正在占用写锁,在释放写锁之前,当前线程又请求了读锁
// 那么在尝试获取读锁的过程中,来到了这一步。假设没有这个 “if (getExclusiveOwnerThread() != current)” 判断的话,这里直接返回-1
// 说明当前线程尝试获取读锁失败,下一步很可能会进入阻塞状态
// 假如当前线程下一步进入了阻塞状态,那么写锁无法释放了,就会导致死锁
return -1;
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} 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");
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;
}
}
}
- doAcquireShared()方法再次获取锁:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED); // 添加共享节点进行等待队列,并返回结点node
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 自旋
final Node p = node.predecessor(); // 获取node的前任结点
if (p == head) { // node的前任结点为头结点,说明node为等待队列中的首个结点
int r = tryAcquireShared(arg); // 再次尝试获取锁
if (r >= 0) { // 获取锁成功,node结点中的线程不需要阻塞
setHeadAndPropagate(node, r); // 设置node为新的头结点,并唤醒下一个共享结点
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 前任结点不是头结点,或者再次尝试获取锁失败
if (shouldParkAfterFailedAcquire(p, node) && // 检查并更新结点状态,判断结点中的线程是否应该阻塞
parkAndCheckInterrupt()) // 阻塞线程
interrupted = true;
}
} finally {
if (failed)
// failed为true,表示发生异常,非正常退出
// 则将node节点的状态设置成CANCELLED,表示node节点所在线程已取消,不需要唤醒了
cancelAcquire(node);
}
}
- addWaiter()方法添加等待节点进行队列:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 基于当前线程和共享锁模式新建结点node
Node pred = tail;
if (pred != null) { // 尾结点不为空,则将node节点加入队列末尾
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); // 尾结点为空,则初始化尾结点
return node;
}
private Node enq(final Node node) {
for (;;) { // 自旋
Node t = tail;
if (t == null) { // 尾结点为空,则初始化头尾节点
if (compareAndSetHead(new Node()))
tail = head;
} else { // 尾结点不为空,则添加node节点进行队列末尾。注意,此处构造了一个空的头结点。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- setHeadAndPropagate()方法设置新的头结点,并唤醒下一个共享结点:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node); // 设置node为新的头结点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) { // 当前结点node成功获取到锁
Node s = node.next; // 获取node的后继结点
if (s == null || s.isShared()) // 后继节点为空,或者为共享结点
doReleaseShared(); // 释放锁,并唤醒后继结点
}
}
- shouldParkAfterFailedAcquire()方法检查并更新结点状态,判断结点中的线程是否应该阻塞:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // node节点的前任节点的waitStatus属性为SIGNAL,说明当前节点中包含的线程应该等待。返回true,阻塞线程
return true;
if (ws > 0) { // waitStatus>0说明为CANCEL,则前任节点处于取消状态。
do { // 向前一直找到未取消的节点为止
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 此处waitStatus必为0或者PROPAGATE,不可能是CONDITION状态
// CONDITION(这个是特殊状态,只在condition列表中节点中存在,独占锁队列中不存在这个状态的节点)
// 将前任结点的状态设置成SIGNAL,这样在下一次循环时,就是直接阻塞当前线程
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false; // 返回false,不阻塞线程
}
- parkAndCheckInterrupt()方法阻塞线程:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞当前线程
return Thread.interrupted(); // 当前线程被唤醒后,返回当前线程中断状态
}
释放锁方法releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 尝试释放锁
doReleaseShared(); // 尝试释放锁失败,再次释放锁
return true;
}
return false;
}
- doReleaseShared()方法释放锁,唤醒线程:
private void doReleaseShared() {
for (;;) { // 自旋
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { // 头结点的waitStatus为SIGNAL,说明后继结点应该是阻塞状态
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 更新头结点状态为0,更新成功则唤醒后继结点;更新失败则自旋
continue;
unparkSuccessor(h); // 唤醒后继结点中的线程
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 如果同步队列头head节点发生改变,继续循环,
// 如果没有改变,就跳出循环
if (h == head)
break;
}
}