1. lock接口
Lock接口作为锁的基础接口,具有锁基本功能,下面针对于源码分析一些基本的用法:
public interface Lock {
/**
* 获取锁
* 调用该方法的当前线程将会获取锁,当锁获得之后,从该方法返回
*/
void lock();
/**
* 可中断的获取锁
* 和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
* @throws InterruptedException
*/
void lockInterruptibly() throws InterruptedException;
/**
* 尝试非阻塞的获取锁,调用该方法后立刻返回,如果可以获取则返回true,否则返回false
* @return
*/
boolean tryLock();
/**
* 超时的获取锁,当前线程在以下3种情况会返回:
* 1. 当前线程在超时时间内获取锁
* 2. 当前线程在超时时间内被中断
* 3. 超时时间结束,返回false
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
*/
void unlock();
/**
* 获取等待通知组件,该组件和当前的锁绑定,当前线程之后获得了锁,才可以调用该组件的wait()方法
* 调用之后,当前线程将释放锁
* @return
*/
Condition newCondition();
}
该接口的实现有两大类锁(独占锁、共享锁)、三小类锁
lock的实现 | 大类 |
---|---|
ReentrantLock | 独占锁 |
ReentrantReadWriteLock.ReadLock | 共享锁 |
ReentrantReadWriteLock.WriteLock | 独占锁 |
该接口作为java并发框架中的锁实现的基础接口,上述的两种共享锁及独占锁都是实现于该接口,明白该接口的含义,意义重大。
2. AQS
1. 为什么?
很多学习者亦或是开发框架都经常看到AQS!为什么该类使用场景如此广泛?因为最初并发框架设计者的初衷就是设计出能够供所有并发功能共用的、能够作为并发基石的类来使用,编程者可以借用该类实现自己的并发工具,主要是面向锁的实现者。
2. 怎么做?
并发设计大佬设计时主要从以下三点出发:
- 在该类中设置了多种
protected
的方法及一个共用的state
的字段,state
的含义并非固定的,而是根据不同的实现定义不同的含义,同时提供了公共的getState()、setState(int) 、 compareAndSetState(int, int) 等方法获取和设置对应的状态,并且可以保证都是线程安全的。 - 利用FIFO(先进先出)的等待队列,处理一些正在获取资源的线程。
- 提供了独占、共享模式,具体的实现由子类完成,该类主要提供一些公有的方法(固定好),同时提供了可以自由发挥的方法(可扩展性)。
3. 需要子类实现的方法(个性)
实现者需要继承AQS并且重写基本的方法,在每个具体的实现中都会调用子类实现模板方法,这里只是按照官方文档梳理一下具体的方法,针对每个方法的源码分析,稍后会详细的给出。
主要的模板方法:
基本方法 | 含义 |
---|---|
protected boolean tryAcquire(int arg) | 独占式的获取同步状态 |
protected boolean tryRelease(int arg) | 独占式的释放同步状态 |
protected int tryAcquireShared(int arg) | 共享式的获取同步状态 |
protected boolean tryReleaseShared(int arg) | 共享式的释放同步状态 |
protected boolean isHeldExclusively() | 当前线程是否在独占模式下被线程占用 |
4. 提供的模板方法(共性)
在锁的实现者实现自定义的同步组件时,会调用AQS提供的模板方法,可以看到如下的方法都是public fina
的方法,也就意味着这些方法都不可以被实现。该方法大致的描述如下:
基本方法 | 含义 | 模式 |
---|---|---|
public final void acquire(int arg) | 独占式的获取同步状态 | 独占模式 |
public final void acquireInterruptibly(int arg) | 独占式、可中断的获取同步状态 | 独占模式 |
public final boolean tryAcquireNanos(int arg,long nanosTimeout) | 独占式、可中断的、具有超时限制的、获取同步状态 | 独占模式 |
public final void acquireShared(int arg) | 共享式的获取同步状态 | 共享模式 |
public final void acquireSharedInterruptibly(int arg) | 共享式、可中断的获取同步状态 | 共享模式 |
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 共享式、可中断的、具有超时限制的、获取同步状态 | 共享模式 |
public final boolean release(int arg) | 独占式的释放同步状态 | 独占模式 |
public final boolean releaseShared(int arg) | 共享式的释放同步状态 | 共享模式 |
public final boolean hasQueuedThreads() | 获取等待在同步队列上的集合 | 查询同步队列等待线程 |
3. 源码分析
分析同步器的具体实现可以从上面的方法出发,结合源码,可以从以下几个方面展开:同步队列、独占式同步状态获取与释放、共享式同步状态的获取与释放、超时获取同步状态,将主要的分析思路放在AQS的共性上。
1. 同步队列
分析源码之前需要首先了解什么是同步队列?
为什么?
同步队列主要用于完成同步状态的管理。
怎么做?
主要用于存放没有获取到同步状态的线程,教材上原话是:“当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息包装成一个节点(Node)并将其加入同步队列中,同时会阻塞当前线程;等到同步状态释放时,会将首节点中的线程唤醒,使其尝试获取同步状态。”
第一次读取原话可能会不太了解具体的含义,结合生活中实际的小例子:
类似于去海底捞吃火锅,去火锅店之后,经常会去前台领取牌号,等着去桌子(同步状态,通常意义的锁)上吃饭,服务员会领客户(当前线程)去候客区(同步队列)等待,比如玩会儿五子棋之类的,等到有顾客吃完回家(同步状态释放),心急的客户可能会问客服是否到自己了(尝试获取同步状态);如果刚好到自己了,则恰饭去(获取同步状态),否则仍然在候客区等候(继续在同步队列等待)。
同步队列节点
同步队列中的节点结构一般包括:获取同步状态失败的线程引用、等待状态、前驱及后继节点、节点的属性类型等基本信息。
节点用一个静态的内部类表示,具体的信息如下面代码:
/**
* FIFO等待队列中每个节点的类
*/
static final class Node {
/**
* 标记该节点为共享节点
* */
static final Node SHARED = new Node();
/**
* 标记该节点为独占节点
* */
static final Node EXCLUSIVE = null;
/**
* 当前节点所在的线程已经被取消
* 通常是由于在同步队列中等待的线程等待超时或者中断
* 节点进入该状态以后将不再改变
* */
static final int CANCELLED = 1;
/**
* 当前节点的后继节点线程将要或者已经被阻塞,而当前节点的线程如果释放了同步状态或者被取消
* 会通知后继节点,使得后继节点得以运行
* 在当前节点释放的时候需要unpark(唤醒)后继节点;
* */
static final int SIGNAL = -1;
/**
* 当前节点在等待队列中,节点线程等待在condition上,当其他线程对Condition调动signal()方法时,
* 该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中
* */
static final int CONDITION = -2;
/**
* releaseShared模型下,需要被传播给后续节点,主要是保证节点继续往后传播
*/
static final int PROPAGATE = -3;
/**
* 表示每个等待节点的状态、分别从上面的代码中取
* 非负数:表示节点已经被取消
* 该只默认为0
*/
volatile int waitStatus;
/**
* 前继节点
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
/**
* 队列中节点所归属的线程
*/
volatile Thread thread;
/**
/**
* 存储condition队列中的后继节点
*/
Node nextWaiter;
/**
* 下一个等待的节点是否是共享节点,是则返回true,否则返回false
* @return
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回前驱节点
* @return
* @throws NullPointerException
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
/**
* 用于addWaiter()方法 主要是加入下一个节点
* @param thread
* @param mode
*/
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
/**
* 主要用于Condition类
* @param thread
* @param waitStatus
*/
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
同步器关键节点
AQS给等待队列提供了几个关键的内容,分别是等待队列的头结点、尾结点、同步器状态、以及该状态的更新及设置方法。
/**
* 等待队列的头结点,只有在使用的时候才初始化(懒加载),只能通过setHead()设置
*/
private transient volatile Node head;
/**
* 等待队列的尾结点 懒加载,只有通过enq()方法添加到等待队列的尾部
*/
private transient volatile Node tail;
/**
* 表示节点的状态(同步的)
* 不同的实现类有不同的含义,本身是线程可见的
*/
private volatile int state;
针对于不同状态之间的更新及设置,同步器AQS本身提供了线程安全的更新方法。
/**
* 获取当前同步器节点状态
* @return
*/
protected final int getState() {
return state;
}
/**
* 设置节点状态
* @param newState
*/
protected final void setState(int newState) {
state = newState;
}
/**
* 通过CAS方式原子性的节点的状态。CAS方法主要是底层帮忙实现
* stateOffset:底层汇编的内存偏移量(如果想知道更详细可查看CAS知识)
* @param expect 内存中现在应该属于的值
* @param update 更新的值
* @return
*/
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
同步队列结构
同步队列及同步器结构示意图如下图所示:
同步器中包含了两个节点的引用:一个是头节点、一个是尾节点。前面说到如果同步状态已经被其他线程所占用,当前线程无法获取同步状态,可以将其构造成节点并且加入到同步队列中。具体的加入同步对队列尾部的方法可以参考之前的数据结构中的双向链表,思路与将元素添加到双向链表尾部类似,加入到同步队列中的逻辑与双向链表不同之处在于同步队列添加到队尾必须保证线程安全。,该处主要通过CAS机制添加到队尾。
如何添加到队尾
如何添加到队头
什么时候会设置同步队列的头节点呢?
如果前头节点是同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒头节点的后继节点,如果后继节点将会在获取同步状态成功时将自己设置为头节点。(获得锁、成为头节点;释放锁,后继节点尝试获取锁,努力成为头节点)
2. 模板方法分析
1. 独占式方法
acquire(int arg)
源码如下:
目的:非中断式的、独占式的获取锁
/**
* @param arg
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
获取思路可分为以下几步:
tryAcquire(arg)
调用尝试获取锁的方法(需要子类实现),如果获取锁成功,则直接返回,否则走下一步(加入到同步队列)addWaiter(Node.EXCLUSIVE)
将当前节点信息构成一个节点并加入到同步队列的尾部- 调用
acquireQueued(Node node,int arg))
获取同步状态(死循环方式) - 如果前两步都不无法获取锁,则阻塞节点中的线程。(被阻塞的线程何时释放呢?被阻塞线程节点的前驱接单释放锁或者阻塞线程节点被中断)
具体的过程流转如下:
同步红色部分为节点自旋过程,直到获取同步状态成功为止。
下面分别看一下上面三个方法:
tryAcquire(int arg)
/**
* 尝试获取独占锁。只有独占模式采用
* 具体的实现由其子类实现
* 实现该方法需要查询当前状态并且判断同步状态是否符合预期、然后再进行CAS设置同步状态
* @param arg
* @return
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
addWaiter(Node mode)
目的:如果同步状态获取失败,则在当前线程中创建一个Node,然后将节点添加到同步队列尾部
/**
* 基本思路:
* 尾结点是否为空,不为空,则利用CAS机制添加到队尾
* 为空则直接调用enq(node),添加节点
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 通过CAS方式、线程安全的将节点添加到队列尾部
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
enq(final Node node)
接着上一步的addWaiter()方法,如果队尾为空,则通过该方法添加节点
/**
* 该方法作用:向同步队列尾部插入一个节点
*
* 如果队尾为空,则将头、尾节点都初始化,然后再次循环
* 如果队尾不为空,尝试CAS来设置队尾。将node节点设置成新的队尾;
* 如果设置不成功,一直自旋,直到成功为止
*
* 核心:通过死循环保证节点的添加成功
* @param node 待添加的节点
* @return node's predecessor 返回node节点的前节点
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果tail为null,则必须创建一个Node节点并进行初始化
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued(final Node node, int arg)
节点加入到同步队列之后,进入一个自旋的过程,达到条件的线程都一直在尝试获取同步状态,一旦获取到同步状态(或者说锁),从自旋中退出;否则一直尝试去获取,并且被唤醒的节点在唤醒之后需要检查自己的前驱节点是否是头节点。
问题1:什么是特定条件?
只有当前驱节点是头节点(持有同步状态)才可以尝试获取锁。
(类似于海底捞等桌子的时候,你领取的号是111,只有当服务员小姐姐叫到了110号,你前去问啥时候到我了,什么时候进去吃呢才有意义。否则的话,如果现在正是排到了100,一直问服务员也没有意义,因为前面还有10多位呢)
具体代码如下:
/**
* 死循环的尝试获取锁,直到成功为止,最后返回中断标志位。
* 如果获取不到则阻塞节点中的线程,而被阻塞的线程的唤醒主要靠前驱节点的出队和阻塞线程被中断实现。
* 思路:
* 在循环中尝试获取锁
* 1. 获取当前节点的前继节点
* 1. 如果前继节点是head,则尝试获取锁(该方法具体的由子类实现),如果获取成功
* 1. 设置当前节点为head节点,清除之前的head,返回中断标志位(此刻不中断)
* 2. 如果p不是head或者获取锁失败,判断是否需要进行park,及判断是否中断
* 2. 最后如果自旋获取锁一直失败,取消获取锁的过程,将该节点标记为取消状态。
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
release(int arg)
线程获取锁之后,执行相应的逻辑之后就需要释放同步状态,使得该节后的后继节点能够获取同步状态。
/**
* 独占式的释放锁,由各个子类的unlock()调用,该方法在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
* 思路:
* 1. 首先会尝试释放锁,失败,则直接返回,成功,则调用第二步
* 2. 成功了之后要去唤醒头节点的后继节点所在的线程,这样后继节点线程才有机会尝试获取同步状态
* @param arg
* @return
*/
public final boolean release(int arg) {
/** 尝试释放锁 */
if (tryRelease(arg)) {
/** 释放成功后unpark后继节点的线程 */
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
2. 共享式方法
acquireShared(int arg)
思路如下:
- 共享的获取同步状态,内部其实调用的tryAcquireShared()方法,
- 如果未获取同步状态,则进入同步队列。
/**
* 与独占的获取方式区别在于同一时刻可以有多个线程获取同步状态。
* @param arg
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared(int arg)
尝试共享式获取同步状态,如果返回值>=0,表示获取成功,反之,获取失败。
该方法需要子类实现
/**
* @param arg
* @return
*/
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
doAcquireShared(int arg)
思路:
- 获取当前节点的前驱节点,判断是否为头节点,是,则尝试获取同步状态;如果获取成功,则唤醒后继节点,并从自旋中退出。
- 如果不是头节点,则判断是否需要等待,并且检查是否中断。然后一直自旋中等待。
- 如果最后获取同步状态成功或者中断,跳出循环,并且判断是否获取锁成功,如果还是失败,则调用cancelAcquire(node)方法,放弃获取同步状态。
源码如下:
/**
* 共享、非中断的获取同步状态
* 首先获取当前节点的前驱节点,如果为头节点,则尝试获取同步状态,如果成功获取,则从自旋中退出。
* @param 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();
// 判断前驱节点是否是头节点
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
// help GC
p.next = null;
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
该方法中有四个方法需要稍微解释一下原理,
分别为
- .
setHeadAndPropagate(node, r)
; 设置同步队列头节点,并唤醒后继节点 - .
shouldParkAfterFailedAcquire(p, node)
在获取锁失败之后是否需要阻塞当前节点线程 - .
parkAndCheckInterrupt()
阻塞当前线程并判断是否应该中断 - .
cancelAcquire(node)
取消获取同步状态操作
1. setHeadAndPropagate(node, r)
在获取共享锁(同步状态)成功,之后则需要将获取锁(同步状态)所在的线程节点设置为头节点,并且唤醒头节点之后的节点。
/**
* 将node节点设置为头节点
* if中的做如下判断,此处的if判断条件比较复杂,但是重点只需要关注如下两点
* 1. 如果propagate > 0,也就是上一步获取锁成功,该值为1,否则为-1
* 2. 如果s == null || s.isShared()则释放节点并唤醒
* @param node
* @param propagate
*/
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
//获取当前节点后继节点
Node s = node.next;
//后继节点为空,则释放后继节点
if (s == null || s.isShared())
doReleaseShared();
}
}
doReleaseShared()
释放后继节点,整个方法的最终目的将后继接地那改成PROPAGATE状态
/**
* 共享模式下的释放节点操作。整个方法都是在一个死循环中操作。
*
* 1. 获取头结点,如果头结点为空,说明整个队列为空,直接跳过
* 2. 如果头结点不为null,并且头结点不是尾结点,获取该节点的状态
* 1. 如果节点状态为signal,试图将该节点状态由SIGNAL改为0,如果失败,则继续重试,否则释放该节点的后继节点
* 2. 如果节点状态为0,并且将该节点状态由0改为PROPAGATE;失败,则继续重试。
* 3. 如果头结点有变化,则继续循环
*
*/
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//唤醒后继节点
unparkSuccessor(h);
} else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
为什么不直接把SIGNAL设置为PROPAGATE,而是先把SIGNAL设置为0,然后再设置为PROPAGATE呢?
答:原因在于unparkSuccessor方法,该方法会判断当前节点的状态是否小于0,
如果小于0则将h的状态设置为0,如果在这里直接设置为PROPAGATE状态的话,则相当于多做了一次CAS操作。
unparkSuccessor(Node node)
目的:当前线程被释放之后,需要唤醒下一个状态为signal节点的线程
/**
* 思路:
* 1. 获取当前节点的状态,如果小于<0,则将该状态设置为0,也就是初始状态
* 2. 获取下一个节点
* 1. 如果next为空或者其状态>0(也就是取消状态),需要从尾部节点往node节点遍历(也就是从后往前遍历),寻找状态为signal的节点
* 2. 如果不为空或者上一步寻找到的节点,通过unpark()方式唤醒该节点
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//寻找状态为signal的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果不为空,则直接唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
question:为什么2.1中需要从从尾部往前遍历,而不是该节点往尾部遍历呢?
回想一下cancelAcquire方法的处理过程,cancelAcquire只是设置了next的变化,没有设置prev的变化,
在最后有这样一行代码:node.next = node,如果这时执行了unparkSuccessor方法,并且向后遍历的话,
就成了死循环了,所以这时只有prev是稳定的。
2. shouldParkAfterFailedAcquire(p, node)
目的:在获取锁失败之后,是否应该阻塞当前线程
/**
* 结论:只有在状态为SIGNAL的时候,才需要阻塞
* 1. 获取前驱节点的状态
* 1. 如果前驱节点为SIGNAL,则需要park
* 2. 如果ws > 0,表示已被取消,需要删除该节点,直到找到状态不为CANCEL的节点
* 3. 其他情况,设置前继节点的状态为SIGNAL。
* @param pred
* @param node
* @return
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
// 状态大于0,表示为取消状态,需要删除该节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 然后将pre节点状态改成SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
3. parkAndCheckInterrupt()
判断阻塞当前线程时候,是否应该中断。
/**
* 判断是否中断
* 阻塞当前线程,然后返回线程的中断状态并复位中断状态。
*
* 1. 如果当前线程是非中断状态,则在执行park时被阻塞,这是返回中断状态是false;
* 2. 如果当前线程是中断状态,则park方法不起作用,会立即返回,然后parkAndCheckInterrupt()方法会获取中断的状态,也就是true,并复位;
* 3. 再次执行循环的时候,由于在前一步已经把该线程的中断状态进行了复位,则再次调用park方法时会阻塞。
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
//park()方法相当于阻塞当前线程
LockSupport.park(this);
return Thread.interrupted();
}
4. cancelAcquire(node )
取消获取同步状态
/**
* 将该节点标记为取消状态,在循环的过程中出现了异常,则执行cancelAcquire方法,
* 思路:
* 1. 如果节点不存在则直接返回
* 2. 获取前驱节点,并且判断其状态是否为取消状态,则向前遍历队列,直到遇到第一个waitStatus <= 0的节点,
* 并把当前节点的前继节点设置为该节点,然后设置当前节点的状态为取消状态。
* 5.
* @param node the node
*/
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
Node pred = node.prev;
// 如果为删除状态,则向前遍历直到遇到非取消状态节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
/**
* If we are the tail, remove ourselves.
* 1.如果当前节点是tail:
* 尝试更新tail节点,设置tail为pred;
* 更新失败则返回,成功则设置tail的后继节点为null
*/
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
/**
* 2.如果当前节点不是head的后继节点,也不是tail节点:
*
* 判断当前节点的前继节点的状态是否是SIGNAL,如果不是则尝试设置前继节点的状态为SIGNAL;
* 上面两个条件如果有一个返回true,则再判断前继节点的thread是否不为空;
* 若满足以上条件,则尝试设置当前节点的前继节点的后继节点为当前节点的后继节点,也就是相当于将当前节点从队列中删除
*/
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
&& pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
//将node的前继节点的next指向了node的后继节点,也就是将node接地那删除掉
compareAndSetNext(pred, predNext, next);
} else {
/** 3.如果是head的后继节点或者状态判断或设置失败,则唤醒当前节点的后继节点 */
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
releaseShared(int arg)
共享式下释放同步状态
/**
* 共享模式下,释放锁。
* 尝试释放共享节点,如果成功则执行释放和唤醒后继等待节点
* tryReleaseShared()由各个子类自己实现
* @param arg
* @return
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
doReleaseShared()
共享模式下的释放节点操作。整个方法都是在一个死循环中操作,该方法的最终目的是将节点都改成PROPAGATE状态。
该方法已经在前面获取释放锁时讲解过。
/**
* 1. 获取头结点,如果头结点为空,说明整个队列为空,直接跳过
* 2. 如果头结点不为null,并且头结点不是尾结点,获取该节点的状态
* 1. 如果节点状态为signal,试图将该节点状态由SIGNAL改为0,如果失败,则继续重试,否则释放该节点的后继节点
* 2. 如果节点状态为0,并且将该节点状态由0改为PROPAGATE;失败,则继续重试。
* 3. 如果头结点有变化,则继续循环
*/
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//唤醒后继节点
unparkSuccessor(h);
} else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
3. 独占式、超时获取同步状态
tryAcquireNanos(int arg, long nanosTimeout)
该方法目的是在一定时间内获取同步状态,没有获取到同步状态,返回false,获取成功则返回true。其内部主要由tryAcquire(arg)
和doAcquireNanos(int arg, long nanosTimeout)
,第一个方法需要子类实现,在之前已经讲过,第二个方法则执行真正的获取同步状态的逻辑。
/**
* 在acquireInterruptibly()基础上加上超时限制.
* 当前线程在特定的时间内没有获取到同步状态,返回false,获取成功则返回true
* @param arg
* @param nanosTimeout 超时时间
* @return
* @throws InterruptedException
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
// 如果当前线程被中断,则直接抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
doAcquireNanos(int arg, long nanosTimeout)
在指定的时间内获取独占锁,如果获取到同步状态,则返回true,否则返回false。
在支持响应中断的基础上,增加了超时获取的特性。
详细思路可以查看代码中的注解:
/**
* 思路:
* 1. 在自旋过程中,当节点的前驱节点为头节点时,则尝试获取同步状态(锁),如果获取成功,则从该方法返回。
* 2. 如果同步锁获取失败,则判断剩余的时间间隔,如果小于0,表示超时,直接返回。
* 2.1 如果没有超时,则使线程等待,
* 3. 判断当前线程是否中断,如果中断则抛出异常
*
* @param arg
* @param nanosTimeout 睡眠时间的间隔
* 该参数的计算公式:nanosTimeout = 进入时间 + nanosTimeout - 当前时间;
* now为当前唤醒时间、lastTime为上次唤醒时间
* nanosTimeout大于0,表示超时时间未到,需要继续睡眠
*
* @return
* @throws InterruptedException
*/
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
// 计算超时时间(绝对时间)
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 1. 如果是头节点并且获取锁成功,退出,返回结果
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 2. 第一步不成功,计算剩余可执行时间
nanosTimeout = deadline - System.nanoTime();
// 3. 判断是是否超时,如果超时直接返回
if (nanosTimeout <= 0L)
return false;
// 4. 执行到这步说明未超时,判断是否需要等待,并且剩余时间是否小于自旋所需最小值
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
将上述流程绘制流程图如下:
总结
至此,AQS提供的所有模板方法都已经解释完毕,这些方法都是可以供子类直接使用,锁的实现者可以直接利用该接口实现自己的类,并且对于锁的使用者而言都是不感知的。并发库本身也多次利用AQS实现一些基础的并发组件比如:ReentrantLock、Semaphore等等,在内部的同步器中实现了AQS类,实现锁本身的工具使用。