目录
第一步:boolean tryAcquire(int arg):尝试去获取同步状态 / 锁 【需要子类去覆盖,重写里面的判断逻辑;获取同步状态成功返回true,失败返回false】
acquire在未获取同步状态时,将会使当前线程一直处于等待状态(直到被唤醒,又继续循环,继续等待);
2.2.3 队列同步器AQS源码分析之共享模式【同一时刻能有多个线程同时获取同步状态】
二、队列同步器AQS
队列同步器 AbstractQueuedSynchronizer (简称:同步器、AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。
同步器从其名字就可以看出来它是一个抽象类。因此,同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
同步状态的改变主要通过同步器提供的三个方法:getState()、setState(int newState) 和compareAndSetState(int expect, int update)。
子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅定义了若干同步状态获取和释放的方法来供自定义同步组件使用。
同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantReadWriteLock 和 CountDownLatch)。
- 同步器与锁的关系:
同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。
可以这样理解二者的关系:锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;而同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待和唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需要关注的领域。
2.1 队列同步器的接口和示例
同步器的设计是基于模板方法模式的,即使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将调用使用者重写的方法。
同步器可重写的方法如下表所示:独占式获取同步状态 tryAcquire-tryRelease;共享式获取同步状态 tryAcquireShared-tryReleaseShared
同步器提供的模板方法如下表所示:
- 独占式获取同步状态 acquire-release;它们会调用重写的tryAcquire-tryRelease
- 共享式获取同步状态 acquireShared-releaseShared;它们会调用重写的tryAcquireShared-tryReleaseShared
上表中的同步器模板方法基本上分为3类:独占式获取和释放同步状态、共享式获取和释放同步状态和查询同步队列中的等待线程情况。
2.2 对同步队列AQS的源码分析
下面将从AQS的源码分析队列同步器AQS是如何完成线程同步的,主要包括:同步队列、独占式同步状态获取和释放、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据结构与模板方法。
2.2.1 同步队列
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态管理。当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程;当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。节点的属性类型与名称以及描述如下表所示:waitStatus , prev, next, thread 【nextWaiter是属于等待队列的】
节点是构成同步队列的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。同步队列的基本结构如下图所示:【head要么是一个初始点(不与任何线程相关,因为第一个入同步队列的节点,会先创建head,然后才在后面再把这个线程放入队列),要么是当前持有锁的节点】【所以准确来说,head是不属于同步队列【放的是被阻塞的节点】的】
获取同步状态失败【那获取成功呢?获取成功就不用加入队列,此时队列里连head可能都没有】: 当一个线程成功地获取了同步状态时,其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect, Node update),只有设置成功后,当前节点才正式与之前的尾结点建立连续。
释放同步状态:同步队列遵循FIFO(先进先出),首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。【设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置首节点的方法并不需要使用CAS来保证。】
- Node的源码
节点类Node是AQS中的一个静态内部类,源码如下:
static final class Node {
//其中SHARED和EXCLUSIVE常量分别代表共享模式和独占模式,所谓共享模式是一个锁允许多条线程同时操
//作,如信号量Semaphore采用的就是基于AQS的共享模式实现的,而独占模式则是同一个时间段只能有一个线
//程对共享资源进行操作,多余的请求线程需要排队等待,如ReentranLock。
static final Node SHARED = new Node(); // 表示当前线程以共享模式持有锁
static final Node EXCLUSIVE = null; // 表示当前线程以独占模式持有锁
static final int CANCELLED = 1; // 表示当前节点已经取消获取锁
static final int SIGNAL = -1; // 表示后继节点的线程需要运行
static final int CONDITION = -2; // 表示当前节点在条件队列中排队
static final int PROPAGATE = -3; // 表示后继节点可以直接获取锁
volatile int waitStatus; // 表示当前节点的等待状态
volatile Node prev; // 同步队列中当前节点的前驱结点
volatile Node next; // 同步队列中当前结点的后继节点
volatile Thread thread; // 当前节点的线程引用
Node nextWaiter; // 表示条件队列中的后继节点
// 判断当前节点是否是共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 返回当前节点的前驱节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 构造器1:无参构造器
Node() {
}
// 构造器2:默认构造器
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
// 构造器3:在条件队列中使用
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
- AQS中的成员变量
AQS的成员变量只有三个,分别是同步队列头结点引用,同步队列尾结点引用以及同步状态。
注意,这三个成员变量都使用了 volatile 关键字进行修饰,这就确保了多个线程对它的修改都是内存可见的。整个类的核心就是这个同步状态state
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
static final class Node{
// ...
}
...
// 同步队列的头结点
private transient volatile Node head;
// 同步队列的尾结点
private transient volatile Node tail;
// 同步状态
private volatile int state;
// 获取同步状态
protected final int getState() {
return state;
}
// 设置同步状态
protected final void setState(int newState) {
state = newState;
}
// 以CAS方式设置同步状态
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
...
}
2.2.2 队列同步器AQS源码分析之独占模式
1、独占式获取同步状态(不响应中断)
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。
主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果失败则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node, int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到 则阻塞 节点中的线程【挂起】,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
-
第一步:boolean tryAcquire(int arg):尝试去获取同步状态 / 锁 【需要子类去覆盖,重写里面的判断逻辑;获取同步状态成功返回true,失败返回false】
/**
* public final void acquire(int arg) {
* if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
* selfInterrupt();
* }
* }
* */
// 尝试去获取同步状态(独占模式)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
-
第二步:addWaiter(Node mode):将当前线程包装成结点并添加到同步队列尾部【先CAS快速尝试添加到尾部,如果失败,说明队列是空的或者有线程在竞争入队,那么就enq方法采取CAS自旋入队尾(如果队列是空,我不能直接入队,要先初始化head和tail,这个head不属于任何线程,然后再将本线程构造的节点入队)】
/**
* public final void acquire(int arg) {
* if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
* selfInterrupt();
* }
* }
* */
// 将当前线程包装成结点并添加到同步队列尾部
private Node addWaiter(Node mode) {
// 指定持有锁的模式
Node node = new Node(Thread.currentThread(), mode);
// 获取同步队列尾结点引用
Node pred = tail;
// 如果尾结点不为空, 表明同步队列已存在结点
if (pred != null) {
// 1.指向当前尾结点
node.prev = pred;
// 2.设置当前结点为尾结点
if (compareAndSetTail(pred, node)) {
// 3.将旧的尾结点的后继指向新的尾结点
pred.next = node;
return node;
}
}
// 否则表明同步队列还没有进行初始化
enq(node);
return node;
}
// 结点入队操作
private Node enq(final Node node) {
for (;;) {
// 获取同步队列尾结点引用
Node t = tail;
// 如果尾结点为空说明同步队列还没有初始化
if (t == null) {
// 初始化同步队列,初始化head和tail
if (compareAndSetHead(new Node())) {
tail = head;
}
} else {
// 1.指向当前尾结点
node.prev = t;
// 2.设置当前结点为尾结点
if (compareAndSetTail(t, node)) {
// 3.将旧的尾结点的后继指向新的尾结点
t.next = node;
return t;
}
}
}
}
-
第三步:acquireQueued(addWaiter(Node.EXCLUSIVE), arg):以独占式获取同步状态 / 锁,直到获取到同步状态才会返回。返回的是是否被中断【通过死循环来不断尝试获取同步状态,每一次自旋:只有前驱节点是头节点才能够尝试获取同步状态。若获取失败,则说明锁的状态还是不可获取--->这时判断是否可以挂起当前线程shouldParkAfterFailedAcquire(p, node),如果判断结果为真则挂起当前线程parkAndCheckInterrupt()。如果不需要挂起则继续下一次自旋, 在这期间线程不响应中断,当头节点的线程释放了同步状态后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。】(什么时候可以挂起呢?我找到了离我最近的waitStatus为SIGNAL的节点,这个状态表示如果当前节点被取消或者释放了同步状态会唤醒后继节点,因为人家会通知我,所以我才能挂起。什么时候不用被挂起呢?当我已经是head的直接后继节点了,我当然不用挂起,我需要不断自旋去尝试获取锁)【挂起用的LockSupport.park(this);】
整个for循环就只有一个出口,那就是等线程成功的获取到同步状态 / 锁之后才能出去,在没有获取到同步状态 / 锁之前就一直是挂在for循环的parkAndCheckInterrupt()方法里(LockSupport.park(this);挂起了这个线程,直到它被前驱节点唤醒,才从这个方法出来)。线程被唤醒后也是从这个地方继续执行for循环。
/**
* public final void acquire(int arg) {
* if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
* selfInterrupt();
* }
* }
* */
// 独占式同步状态获取
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取给定结点的前继结点的引用
final Node p = node.predecessor();
// 如果当前结点是同步队列的第一个结点, 就尝试去获取锁
if (p == head && tryAcquire(arg)) {
// 将给定结点设置为head结点
setHead(node);
// 为了帮助垃圾收集, 将上一个head结点的后继清空
p.next = null;
// 设置获取成功状态
failed = false;
// 返回中断的状态, 整个循环执行到这里才是出口
return interrupted;
}
// 否则说明锁的状态还是不可获取, 这时判断是否可以挂起当前线程
// 如果判断结果为真则挂起当前线程, 否则继续循环, 在这期间线程不响应中断
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
// 在最后确保如果获取失败就取消获取
if (failed) {
cancelAcquire(node);
}
}
}
// 判断是否可以将当前结点挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前继结点的等待状态
int ws = pred.waitStatus;
// 如果前继结点状态为SIGNAL, 表明前继结点会唤醒当前结点, 所以当前结点可以安心的挂起了
if (ws == Node.SIGNAL) {
return true;
}
if (ws > 0) {
// 下面的操作是清理同步队列中所有已取消的前继结点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 到这里表示前继结点状态不是SIGNAL, 很可能还是等于0, 这样的话前继结点就不会去唤醒当前结点了
// 所以当前结点必须要确保前继结点的状态为SIGNAL才能安心的挂起自己
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 挂起当前线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
-
第四步:selfInterrupt():中断当前线程【可选acquireQueued(addWaiter(Node.EXCLUSIVE), arg)直到获取到同步状态才会返回,返回的是是否被中断。没有成功获取到锁之前不会响应任何形式的线程中断,只有当线程成功获取到锁并从for循环出来后,才会查看在这期间是否有人要求中断线程,如果是的话再去调用selfInterrupt()方法将自己挂起。】
/**
* public final void acquire(int arg) {
* if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
* selfInterrupt();
* }
* }
* */
// 当前线程将自己中断
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
2、boolean release(int arg)独占式同步状态的释放:同步器调用tryRelease(int arg)方法释放同步状态,释放成功则会唤醒头节点的后继节点线程(不是取消状态),unparkSuccessor(Node node)方法使用LockSupport.unpark(s.thread)来唤醒处于等待状态的线程。
public final boolean release(int arg) {
// 看是否能成功释放同步状态
if (tryRelease(arg)) {
// 获取head结点
Node h = head;
// 如果head结点不为空并且等待状态不等于0就去唤醒后继结点
if (h != null && h.waitStatus != 0) {
// 唤醒后继结点
unparkSuccessor(h);
}
return true;
}
//没能成功释放同步节点
return false;
}
// 唤醒后继结点
private void unparkSuccessor(Node node) {
// 获取给定结点的等待状态
int ws = node.waitStatus;
// 将等待状态更新为0
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0);
}
// 获取给定结点的后继结点
Node s = node.next;
// 后继结点为空或者等待状态为取消状态
if (s == null || s.waitStatus > 0) {
s = null;
// 从后向前遍历队列找到第一个不是取消状态的结点
for (Node t = tail; t != null && t != node; t = t.prev) {
if (t.waitStatus <= 0) {
s = t;
}
}
}
// 唤醒给定结点后面首个不是取消状态的结点
if (s != null) {
LockSupport.unpark(s.thread);
}
}
3、acquireInterruptibly(int arg)独占式可中断获取同步状态:可响应中断【响应线程中断方式和不响应线程中断方式获取锁流程上大致上是相同的。唯一的一点区别就是线程从parkAndCheckInterrupt方法中醒来后会检查线程是否中断,如果是的话就抛出InterruptedException异常***,而不响应线程中断获取锁是在收到中断请求后只是设置一下中断状态,并不会立马结束当前获取锁的方法,一直到结点成功获取到锁之后才会根据中断状态决定是否将自己挂起】
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);*****
}
// 以可中断模式获取同步状态 / 锁(独占模式)
private void doAcquireInterruptibly(int arg) throws InterruptedException {
// 将当前线程包装成结点添加到同步队列中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
// 获取当前结点的前继结点
final Node p = node.predecessor();
// 如果p是head结点, 那么当前线程就再次尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
//获取锁成功后返回
return;
}
// 如果满足条件就挂起当前线程, 此时响应中断并抛出异常
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
// 线程被唤醒后如果发现中断请求就抛出异常
throw new InterruptedException(); //******
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
4、boolean tryAcquireNanos(int arg, long nanosTimeout)独占式超时获取同步状态:【该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从方法中返回,这个过程和独占式同步获取的过程类似,但是在同步状态获取失败的处理上不同。如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于等于0,就代表已经超时了),如果没有超时,重新计算nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Object blocker, long nanos)方法返回)。注意在以超时时间获取锁的过程中是可以响应线程中断请求的。】
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout); ******
}
// 以限定超时时间获取同步状态/ 锁(独占模式)
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
// 获取系统当前时间
long lastTime = System.nanoTime();
// 将当前线程包装成结点添加到同步队列中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
// 获取当前结点的前继结点
final Node p = node.predecessor();
//如果前继是head结点, 那么当前线程就再次尝试获取锁
if (p == head && tryAcquire(arg)) {
// 更新head结点
setHead(node);
p.next = null;
failed = false;
return true;
}
// 超时时间用完了就直接退出循环
if (nanosTimeout <= 0) {
return false;
}
// 如果超时时间大于自旋时间, 那么等判断可以挂起线程之后就会将线程挂起一段时间
//如果 nanosTimeout > spinForTimeoutThreshold(1000ns)时,
//将不会进行超时等待,而是进入快速自旋过程。因为非常短的超时等待无法做到十分精确,
//若这时再进行超时等待,反而整体上表现得不精确
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) {
// 将当前线程挂起一段时间, 之后再自己醒来
LockSupport.parkNanos(this, nanosTimeout);
}
// 获取系统当前时间
long now = System.nanoTime();
//超时时间每次都减去获取锁的时间间隔
nanosTimeout -= now - lastTime;
// 再次更新lastTime
lastTime = now;
// 在获取锁的期间收到中断请求就抛出异常
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
综:
独占式获取同步状态acquire、可响应中断独占式获取同步状态acquireInterruptibly、独占式超时获取同步状态tryAcquireNanos在流程上非常相似【首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果失败则构造同步节点,并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,然后使得该节点以“死循环”的方式获取同步状态。如果获取不到 则阻塞 节点中的线程【挂起】,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。】,它们得主要区别在于未获取到同步状态时的处理逻辑:
acquire在未获取同步状态时,将会使当前线程一直处于等待状态(直到被唤醒,又继续循环,继续等待);
acquireInterruptibly会在线程被唤醒后检查线程是否中断,如果是的话就抛出 InterruptedException异常***,而不响应线程中断获取锁是在收到中断请求后只是设置一下中断状态,并不会立马结束当前获取锁的方法,一直到结点成功获取到锁之后才会根据中断状态决定是否将自己挂起
tryAcquireNanos会使当前线程等待(挂起)nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒没有获取到同步状态【时间到了,前驱节点出队**或者阻塞线程被中断都会唤醒该线程】,将会从等待逻辑中自动返回。如果时间没到,发生了中断,那么抛出中断异常。
2.2.3 队列同步器AQS源码分析之共享模式【同一时刻能有多个线程同时获取同步状态】
独占模式获取同步状态(或者说获取锁)有三种方式,分别是:独占式同步状态获取(不响应中断)、独占式同步状态获取(响应中断)和超时获取同步状态。在共享模式下获取同步状态的方式也是这三种,而且基本上大同小异。
共享模式获取同步状态与独占模式获取同步状态最主要的区别在于同一时刻能否有多个线程同时获取同步状态。
以文件为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作可以同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问。【如图:左半部分,共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞; 右半部分式独占式访问资源时,同一时刻其他访问均被阻塞】
1、共享式获取同步状态(不响应中断)acquireShared(int arg)::首先调用自定义同步器实现的tryAcquireShared去尝试获取同步状态,大于等于0表示成功,小于0表示失败,如果失败则构造同步节点(共享式Node.SHARED)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,然后该节点以“死循环”的方式获取同步状态。如果获取不到 则阻塞 节点中的线程【挂起】,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。如果当前节点恰巧成功的获取了锁,那么接下来就会调用setHeadAndPropagate方法将自己设置为head节点,并且唤醒后面同样是共享模式的节点。【与独占式的区别就是共享式在获取锁后会唤醒后面同样是共享模式的节点,让后面的节点也能获得锁,而独占式不会】【注意每次唤醒仅仅只是唤醒后一个节点,如果后一个节点不是共享模式的话,当前节点就不会再去唤醒更后面的节点了。】
调用acquireShared()方法是不响应线程中断共享式获取同步状态的方式。在该方法中,首先调用tryAcquireShared去尝试获取同步状态,tryAcquireShared方法返回一个获取同步的状态。这里AQS规定了返回状态若是负数代表当前结点获取同步状态失败,若是0代表当前结点获取同步状态成功,但后继结点不能再获取了,若是正数则代表当前结点获取同步状态成功,并且这个同步状态后续结点也同样可以获取成功。
自定义子类在实现tryAcquireShared方法获取同步状态的逻辑时,返回值需要遵守这个约定。如果调用tryAcquireShared的返回值小于0,就代表这次尝试获取同步状态失败了,接下来就调用doAcquireShared方法将当前线程添加进同步队列
public final void acquireShared(int arg) {
// 1.尝试去获取同步状态
if (tryAcquireShared(arg) < 0) {
// 2.如果获取失败就进入这个方法
doAcquireShared(arg);
}
}
private void doAcquireShared(int arg) {
// 添加到同步队列中,指定节点为共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前结点的前继结点
final Node p = node.predecessor();
// 如果前继结点为head结点就再次尝试去获取锁
if (p == head) {
// 再次尝试去获取同步状态并返回获取状态
// r < 0, 表示获取失败
// r = 0, 表示当前结点获取成功, 但是后继结点不能再获取了
// r > 0, 表示当前结点获取成功, 并且后继结点同样可以获取成功
int r = tryAcquireShared(arg);
if (r >= 0) {
// 到这里说明当前结点已经获取同步状态成功了, 此时它会将锁的状态信息传播给后继结点
setHeadAndPropagate(node, r);
p.next = null;
// 如果在线程阻塞期间收到中断请求, 就在这一步响应该请求
if (interrupted) {
selfInterrupt();
}
failed = false;
return;
}
}
// 每次获取锁失败后都会判断是否可以将线程挂起,
// 如果可以的话就会在parkAndCheckInterrupt方法里将线程挂起
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 将给定结点设置为head结点
setHead(node);
// 如果propagate大于0表明锁可以获取了
if (propagate > 0 || h == null || h.waitStatus < 0) {
// 获取给定结点的后继结点
Node s = node.next;
// 如果给定结点的后继结点为空, 或者它的状态是共享状态
if (s == null || s.isShared()) {
// 唤醒后继结点
doReleaseShared();
}
}
}
//释放同步状态的操作(共享模式)
private void doReleaseShared() {
for (;;) {
// 获取同步队列的head结点
Node h = head;
if (h != null && h != tail) {
// 获取head结点的等待状态
int ws = h.waitStatus;
// 如果head结点的状态为SIGNAL, 表明后面有人在排队
if (ws == Node.SIGNAL) {
// 先把head结点的等待状态更新为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
continue;
}
// 再去唤醒后继结点
unparkSuccessor(h);
// 如果head结点的状态为0, 表明此时后面没人在排队, 就只是将head状态修改为PROPAGATE
}else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
continue;
}
}
// 只有保证期间head结点没被修改过才能跳出循环
if (h == head) {
break;
}
}
}
// 尝试去获取同步状态(共享模式)
// 负数:表示获取失败
// 零值:表示当前结点获取成功, 但是后继结点不能再获取了
// 正数:表示当前结点获取成功, 并且后继结点同样可以获取成功
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
调用setHeadAndPropagate方法首先将自己设置成head节点,然后再根据传入的tryAcquireShared方法的返回值来决定是否要去唤醒后继节点。
前面已经讲到当返回值大于0就表明当前节点成功获取了同步状态,并且后面的节点也可以成功获取同步状态。这时当前节点就需要去唤醒后面同样是共享模式的节点,注意,每次唤醒仅仅只是唤醒后一个节点,如果后一个节点不是共享模式的话,当前节点就不会再去唤醒更后面的节点了。
共享模式下唤醒后继节点的操作是在doReleaseShared方法进行的,共享模式和独占模式的唤醒操作基本也是相同的,都是去找到自己座位上的牌子(等待状态),如果牌子上为SIGNAL表明后面有人需要让它帮忙唤醒,如果牌子上为0则表明队列此时并没有节点在排队。
在独占模式下是如果发现没节点在排队就直接离开队列了,而在共享模式下如果发现队列后面没节点在排队,当前节点在离开前仍然会留个小纸条(将等待状态设置为PROPAGATE)告诉后来的人这个锁的可获取状态。那么后面来的人在尝试获取锁的时候可以根据这个状态来判断是否直接获取锁。
2、共享式同步状态的释放realeaseShared(int arg):通过tryReleaseShared成功释放同步状态后,将会唤醒后续处于等待状态的节点。与独占式的区别主要在于它必须保证同步状态线程安全释放,一般是通过循环和CAS实现,因为释放同步状态的操作会同时来自多个线程。
public final boolean releaseShared(int arg) {
// 1.尝试去释放同步状态
if (tryReleaseShared(arg)) {
// 2.如果释放成功就唤醒其他线程
doReleaseShared(); //见上
return true;
}
return false;
}
// 尝试去释放同步状态(共享模式)
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
获取同步状态(或者说锁)的线程执行完逻辑后就会调用releaseShared方法释放同步状态,首先调用tryReleaseShared方法尝试释放同步状态该方法的判断逻辑由自定义子类覆盖它去实现的。
如果释放成功就调用doReleaseShared方法去唤醒后继结点。后它会找到原先的座位(head结点),看看座位上是否有节点留了小纸条(状态为SIGNAL),如果有就去唤醒后继结点。
如果没有(状态为0)就代表队列没节点在排队,那么在离开之前它还要做最后一件事情,就是在自己座位上留下小纸条(状态设置为PROPAGATE),告诉后面的节点锁的获取状态。
整个释放锁的过程和独占模式唯一的区别就是在这最后一步操作。因为共享式释放同步状态的操作可能会同时来自多个线程,为了保证同步状态线程安全释放,一般是通过循环和CAS保证的。
3、共享式获取同步状态(响应中断)acquireSharedInterruptibly(int arg):会在线程被唤醒后检查线程是否中断,如果是的话就抛出 InterruptedException异常***
4、共享式超时获取同步状态tryAcquireSharedNanos(int arg, long nanosTimeout):
2.2.4 Condition接口、等待队列
1、Condition接口概述
每个Java对象都拥有一组监视器方法,主要包括:wait()、wait(long timeout)、notify() 和 notifyAll()方法,这些方法与synchronized关键字配合,可以实现等待 / 通知模式。
Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待 / 通知模式,但是这两者在使用方法和功能特性上还是有差别的。
Condition定义了 等待 / 通知 的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,即Condition对象是依赖Lock对象的。
一般会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
public class ConditionUseCase {
Lock lock = new ReentrantLock();
// 通过lock.newCondition()方法创建condition对象
Condition condition = lock.newCondition();
// 等待:conditionWait()
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await(); // 当前线程进入等待状态
} finally {
}
}
// 通知:conditionSignal()
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal(); // 唤醒一个等待在Condition上的线程
} finally {
lock.unlock();
}
}
}
Condition的主要方法如下表所示:
1.1 Condition接口源码
public interface Condition {
// 响应线程中断的条件等待
void await() throws InterruptedException;
// 不响应线程中断的条件等待
void awaitUninterruptibly();
// 设置相对时间的条件等待(不进行自旋)
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 设置相对时间的条件等待(进行自旋)
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 设置绝对时间的条件等待
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒条件队列中的头节点
void signal();
// 唤醒条件队列的所有节点
void signalAll();
}
调用signal方法一定会将线程从等待队列中移到同步队列尾部。
await方法分为5种,分别是:响应线程中断等待、不响应线程中断等待、设置相对时间不自旋等待、设置相对时间自旋等待、设置绝对时间等待;
signal方法只有2种,分别是:只唤醒条件队列头节点和唤醒条件队列所有节点。
2、Condition源码分析【等待队列、等待和通知】
ConditionObject是同步器AQS的内部类,它实现了Condition接口。每个Condition对象都包含着一个队列(以下称之为等待队列),该队列是Condition对象实现 等待 / 通知 功能实现的关键。【我们通过lock.newCondition();返回一个 new ConditionObject();对象】
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
private transient Node firstWaiter; //****
private transient Node lastWaiter; //****
public ConditionObject() { }
.......
}
下面将分析Condition的实现,主要包括:等待队列、等待和通知,下面提到的Condition如果不加说明均指的是ConditionObject。
2.1 等待队列【在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包JUC中的Lock拥有一个同步队列和多个等待队列(通过创建多个condition对象:一个Condition包含一个等待队列,Condition拥有首节点和尾节点。如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列尾部并进入等待状态,此节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的)。】
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。这里的用的节点类型也是同步器的静态内部类Node。
等待队列可以有多个,按照不同的等待条件而设置不同的等待队列。等待队列是一条单向链表。
Condition拥有尾结点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上诉节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
2.2 await()响应线程中断等待【会使当前线程进入等待队列并释放锁,同时线程状态转变为等待状态(相当于同步队列的首节点(获取了同步状态 / 锁的节点)移动到Condition的等待队列中。依然会阻塞线程LockSupport.park(this)),然后走同步队列抢锁的那一套。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。】
1)节点进入等待队列的步骤:
-
addConditionWaiter():将当前线程添加到条件队列尾部,其中如果发现等待队列尾结点已取消就会调用unlinkCancelledWaiters方法将条件队列所有的已取消结点清空?。并没有挂起线程
-
fullyRelease(node):在挂起线程前要释放所有的锁,唤醒同步队列中的后继节点。【因为锁是可重入的,所以在进行条件等待前需要将锁全部释放了,不然的话别人就获取不了锁了。如果释放锁失败的话就会抛出一个运行时异常,如果成功释放了锁的话就返回之前的同步状态】
-
如果线程不在同步队列中,则while循环进入等待状态:首先调用LockSupport.park(this)将线程挂起了,所以线程就会一直在这里阻塞(直到signal唤醒或者是中断发生,都会将线程从等待队列转移到同步队列,结束循环)。
-
在调用signal方法后仅仅只是将结点从等待队列转移到同步队列中去,至于会不会唤醒线程需要看情况【如果转移节点时发现同步队列中的前驱节点已取消,或者是更新前驱节点的状态为SIGNAL失败,这两种情况都会立即唤醒线程,否则的话在signal方法结束时就不会去唤醒已在同步队列中的线程,而是等到它的前驱节点来唤醒。】。【逻辑在signal里】
-
当线程不阻塞了,会判断是中断引起的还是signal引起的,如果是中断,则代表结点取消条件等待, 将节点从等待队列转移到同步队列【所以说是响应中断的,不响应中断的话只是会设置状态,而不会跳出】
-
2) 节点从等待队列转移到同步队列后的步骤:
- acquireQueued(node, savedState):独占模式以死循环的方式获取锁(直到获取锁才会出来,里面也会阻塞在同步队列里)
-
reportInterruptAfterWait(interruptMode):获取锁后,根据中断情况做相应处理:
-
如果中断发生在signal方法之前,interruptMode就为THROW_IE,再次获得锁后就抛出异常;【结合它一中断就从等待队列出来,说明它是可响应中断的】
-
如果中断发生在signal方法之后,interruptMode就为REINTERRUPT,再次获得锁后就重新中断。
-
// 响应线程中断的条件等待
public final void await() throws InterruptedException {
// 如果线程被中断则抛出异常
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 1、 将当前线程添加到条件队列尾部
Node node = addConditionWaiter();
// 2、 在进入条件等待之前先完全释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 3、 线程一直在while循环里进行条件等待,当它不在同步队列时一直循环
while (!isOnSyncQueue(node)) {
// 进行条件等待的线程都在这里被挂起, 线程被唤醒的情况有以下几种:
// 1.同步队列的前驱节点已取消
// 2.设置同步队列的前驱节点的状态为SIGNAL失败
// 3.前驱节点释放锁后唤醒当前节点
LockSupport.park(this);
// 当前线程醒来后立马检查是否被中断, 如果是则代表结点取消条件等待,
// 此时需要将结点移出条件队列【这个方法会将节点移出并返回是否中断】
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
break;
}
}
//****** 以下是节点已经从等待队列转移到同步队列了
// 线程醒来后就会以独占模式获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
// 这步操作主要为防止线程在signal之前中断而导致没与条件队列断绝联系
if (node.nextWaiter != null) {
unlinkCancelledWaiters();
}
// 根据中断模式进行响应的中断处理
if (interruptMode != 0) {
reportInterruptAfterWait(interruptMode);
}
}
// 完全释放锁
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 获取当前的同步状态
int savedState = getState();
// 使用当前的同步状态去释放锁
if (release(savedState)) {
failed = false;
// 如果释放锁成功就返回当前同步状态
return savedState;
} else {
// 如果释放锁失败就抛出运行时异常
throw new IllegalMonitorStateException();
}
} finally {
// 保证没有成功释放锁就将该节点设置为取消状态
if (failed) {
node.waitStatus = Node.CANCELLED;
}
}
}
// 检查条件等待时的线程中断情况
private int checkInterruptWhileWaiting(Node node) {
// 中断请求在signal操作之前:THROW_IE
// 中断请求在signal操作之后:REINTERRUPT
// 期间没有收到任何中断请求:0
return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
}
// 将取消条件等待的结点从等待队列转移到同步队列中
final boolean transferAfterCancelledWait(Node node) {
// 如果这步CAS操作成功的话就表明中断发生在signal方法之前
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 状态修改成功后就将该结点放入同步队列尾部
enq(node);
return true;
}
// 到这里表明CAS操作失败, 说明中断发生在signal方法之后
while (!isOnSyncQueue(node)) {
// 如果sinal方法还没有将结点转移到同步队列, 就通过自旋等待一下
Thread.yield();
}
return false;
}
// 结束条件等待后根据中断情况做出相应处理
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
// 如果中断模式是THROW_IE就抛出异常
if (interruptMode == THROW_IE) {
throw new InterruptedException();
// 如果中断模式是REINTERRUPT就自己挂起
} else if (interruptMode == REINTERRUPT) {
selfInterrupt();
}
}
2.3 不响应线程中断等待awaitUninterruptibly【与响应的差别在于:await在signal或者中断发生时都会从等待队列转移到同步队列,从而跳出循环结束等待;而awaitUninterruptibly在中断发生时只设置了中断标志,并不会将其从等待队列转移到同步队列,所以依然在循环等待】
// 不响应线程中断的条件等待
public final void awaitUninterruptibly() {
// 将当前线程添加到等待队列尾部
Node node = addConditionWaiter();
// 完全释放锁并返回当前同步状态
int savedState = fullyRelease(node);
boolean interrupted = false;
// 结点一直在while循环里进行条件等待
while (!isOnSyncQueue(node)) {
// 等待队列中所有的线程都在这里被挂起
LockSupport.park(this);
// 线程醒来发现中断并不会马上去响应
if (Thread.interrupted()) {
interrupted = true;
}
}
if (acquireQueued(node, savedState) || interrupted) {
// 在这里响应所有中断请求, 满足以下两个条件之一就会将自己挂起
// 1.线程在条件等待时收到中断请求
// 2.线程在acquireQueued方法里收到中断请求
selfInterrupt();
}
}
2.3 唤醒等待队列中的头节点signal()【拥有锁的线程调用此方法会将等待队列中的首节点移动到同步队列中,然后可能唤醒它(LockSupport.unpark(node.thread),如果转移节点时发现同步队列中的前驱节点已取消,或者是更新前驱节点的状态为SIGNAL失败,这两种情况都会立即唤醒线程,否则的话在signal方法结束时就不会去唤醒已在同步队列中的线程,而是等到它的前驱节点来唤醒。)【被唤醒后,就会从await的LockSupport.park出来,然后调用acquire加入到锁的同步状态竞争中,直到获取锁,才会从await方法返回】】
以看到signal方法最终的核心就是去调用transferForSignal方法,在transferForSignal方法中首先会用CAS操作将节点的状态从CONDITION设置为0,然后再调用enq方法将该节点添加到同步队列尾部。
我们再看接下来的 if 判断语句,这个判断语句主要是用来判断什么时候会去唤醒线程,出现下面这两种情况就会立即唤醒线程:一种是当发现前继结点的状态是取消状态时,还有一种是更新前继结点的状态失败时。
这两种情况都会马上去唤醒线程,否则的话就仅仅只是将节点从条件队列中转移到同步队列中就完了,而不会立马去唤醒节点中的线程。signalAll方法也大致类似,只不过它是去循环遍历条件队列中的所有节点,并将它们转移到同步队列,转移节点的方法也还是调用transferForSignal方法。
// 唤醒条件队列中的下一个结点
public final void signal() {
// 判断当前线程是否持有锁
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException();
}
Node first = firstWaiter;
// 如果等待队列中有排队者
if (first != null) {
// 唤醒等待 队列中的头结点
doSignal(first);
}
}
//唤醒条件队列中的头结点
private void doSignal(Node first) {
do {
//1.将firstWaiter引用向后移动一位
if ( (firstWaiter = first.nextWaiter) == null) {
lastWaiter = null;
}
//2.将头结点的后继结点引用置空
first.nextWaiter = null;
//3.将头结点转移到同步队列, 转移完成后有可能唤醒线程
//4.如果transferForSignal操作失败就去唤醒下一个结点
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
//将指定结点从条件队列转移到同步队列中
final boolean transferForSignal(Node node) {
//将等待状态从CONDITION设置为0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//如果更新状态的操作失败就直接返回false
//可能是transferAfterCancelledWait方法先将状态改变了, 导致这步CAS操作失败
return false;
}
//将该结点添加到同步队列尾部
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {
//出现以下情况就会唤醒当前线程
//1.前继结点是取消状态
//2.更新前继结点的状态为SIGNAL操作失败
LockSupport.unpark(node.thread);
}
return true;
}
2.2.5 示例图分析
下面属于回顾环节,用简单的示例来说一遍,如果上面的有些东西没看懂,这里还有一次帮助你理解的机会。
首先,第一个线程调用 reentrantLock.lock(),翻到最前面可以发现,tryAcquire(1) 直接就返回 true 了,结束。只是设置了 state=1,连 head 都没有初始化,更谈不上什么阻塞队列了。要是线程 1 调用 unlock() 了,才有线程 2 来,那世界就太太太平了,完全没有交集嘛,那我还要 AQS 干嘛。
如果线程 1 没有调用 unlock() 之前,线程 2 调用了 lock(), 想想会发生什么?
线程 2 会初始化 head【new Node()】,同时线程 2 也会插入到阻塞队列并挂起 (注意看这里是一个 for 循环,而且设置 head 和 tail 的部分是不 return 的,只有入队成功才会跳出循环)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
首先,是线程 2 初始化 head 节点,此时 head==tail, waitStatus==0
然后线程 2 入队:
然后线程 2 入队:
同时我们也要看此时节点的 waitStatus,我们知道 head 节点是线程 2 初始化的,此时的 waitStatus 没有设置, java 默认会设置为 0,但是到 shouldParkAfterFailedAcquire 这个方法的时候,线程 2 会把前驱节点,也就是 head 的waitStatus设置为 -1。
那线程 2 节点此时的 waitStatus 是多少呢,由于没有设置,所以是 0;
如果线程 3 此时再进来,直接插到线程 2 的后面就可以了,此时线程 3 的 waitStatus 是 0,到 shouldParkAfterFailedAcquire 方法的时候把前驱节点线程 2 的 waitStatus 设置为 -1。
这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思,Doug Lea 注释的是:代表后继节点需要被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,我们知道,每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL,然后阻塞,等待被前驱唤醒。这里涉及的是两个问题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着 “waitStatus代表后继节点的状态” 这种思路去看一遍源码。