AbstractQueuedSynchronizer是并发包的核心基础类,它是构建阻塞锁和相关同步器(信号量、事件,等等)的框架,内部为FIFO队列,采用的是一种类似CLH锁队列的同步队列。引用他人对CLH的解释,CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
ReentrantLock锁实现类的大部分逻辑,通过内部类Sync 继承AQS 重写部分相关方法实现的。
队列同步器AbstractQueuedSynchronizer(以下简称同步器),AQS 是一个抽象的队列式同步器框架,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO(先进先出)队列来完成资源获取线程的排队工作,JUC 包中的同步类基本都是基于 AQS 同步器来实现的,如 ReentrantLock,Semaphore 等。
1、AQS 工作机制:(三点)
1.1 同步状态的原子性管理:
CAS操作state: unsafe.compareAndSwapInt(this, stateOffset, expect, update); 根据对象的state同步状态偏移量是否和expect值相同,相同则更新。标准的CAS操作。
1.2 线程的阻塞与解除阻塞:
1.3 队列的管理:
2、CLH 队列:
结构类似于:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
......
private transient volatile Node head;//头节点
private transient volatile Node tail;//尾节点
private volatile int state;//*同步状态*
......
static final class Node {
volatile int waitStatus;//等待状态
volatile Node prev;//前驱
volatile Node next;//后继
volatile Thread thread;//线程引用
// 条件队列(只有独占模式)下一个等待的节点或者当为SHARED时表示为共享模式的等待状态
Node nextWaiter;
......
}
......
}
注:Node类型的prev、next属性以及AbstractQueuedSynchronizer类型的head 、tail属性都设置为volatile,保证可见性。
2.1、node节点属性的解析
node节点作为CLH队列的一个节点,有着5条属性,分别是waitStatus 、prev、next、thread、nextWater。
1.waitStatus是当前节点的一个等待状态标志位,该标志位决定了该节点在当前情况下处于何种状态。
Node结点是对每一个访问同步代码的线程的封装,其包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。变量waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。AQS内部的同步状态是通过state表示的,state 被volatile修饰保证了不依赖了state自身的set, get方法操作的可见性。
状态 | 判断结果 | 说明 |
---|---|---|
waitStatus=0 | 代表初始化状态 | 该节点尚未被初始化完成 |
waitStatus>0 | 取消状态 | 说明该线程中断或者等待超时,需要移除该线程 |
waitStatus<0 | 有效状态 | 该线程处于可以被唤醒的状态 |
/*获取当前同步状态*/
protected final int getState() {return state;}
/*设置当前同步状态*/
protected final void setState(int newState) {state = newState;}
/*使用Unsafe的CAS操作设置当前同步状态,该方法操作能保证原子性*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
同步状态 state的具体语义与具体使用者有关。如在Semaphore中state表示可获取的信号量,当调用acquire方法成功获取一次后state值将减一,用完调用release方法释放后state的值将加一 ; 在CountDownLatch中 state表示调用await的方法的线程还要等待调用多少次countDown方法,该线程才能继续执行; 在ReentrantLock中state表示,线程调用lock方法的次数。
2.prve next thread介绍
2.1 父类AbstractOwnableSynchronizer
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
...
private transient Thread exclusiveOwnerThread;
// 设置当前独占的线程
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
// 获取当前独占的线程
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
2.2 AbstractQueuedSynchronizer类
AQS是基本模板方法设计模式实现的。一般使用者根据具体需要,组合一个实现了AQS的子类,该类重写了AQS中的相应的模板方法来满足使用者特定的同步语义。如:CountDownLacth、Semaphore、ReentrantLock、ReentrantReadWriteLock都是采用这种方式实现的。 AQS提供的可重写方法如下:
/* 独占模式(排它模式)获取同步状态,获取同步状态前要检查当前线程是否可以获取。因为可能已被其它线程获取,
* 当同步状态允许被线程多次获取时,独占模式只能被同一个线程多次获取。未获取同步状态的线程,将在同步队列中
* 等待。
*/
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException();}
/*独占模式释放同步状态,方法调用完后在同步队列中等待获取同步状态的线程将有机会获取同步状态*/
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException();}
/*共享模式获取同步状态,返回大于等于0,表示成功获取同步状态,反之代表失败*/
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException();}
/*共享模式释放同步状态*/
protected boolean tryReleaseShared(int arg) {throw new UnsupportedOperationException();}
/*同步器是否在独占模式(排它模式)下被线程占用,一般改方法表示同步状态是否已被当前线程获取过*/
protected boolean isHeldExclusively() {throw new UnsupportedOperationException();}
默认情况这个方法都直接抛出UnsupportedOperationException异常,使用者可以根据情况以独占或是共享的模式来完成具体的同步语义。
实现自定的同步组件时,将会调用AQS提供的模板方法,这些模板方法内部又将调用上面重写的tryAcquire、 tryRelease、 tryAcquireShared 、tryReleaseShared 、isHeldExclusively方法。模板方法描述如下:
/*独占模式获取同步状态,该方法会先调用重写的tryAcquire方法尝试获取同步状态,如果当前线程成功获取同步状态,
*该方法直接返回,否则当前线程会被封装成一个节点放入同步队列中等待
*/
public final void acquire(int arg) {//现实省去...}
/*与acquire方法类似,但该方法能响应中断,如果当前线程未能获取到同步状态,而在同步队列中,当这个线程被中断时,
*则该抛出InterruptedException 异常并返回
*/
public final void acquireInterruptibly(int arg){//现实省去...}
/*在acquireInterruptibly方法上加了超时机制,如果当前线程在超过时间内没有获取同步状态,那么将返回false,
*获取到返回ture
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout){//现实省去...}
/*共享式获取同步状态,该方法会先调用重写的tryAcquireShared方法尝试获取同步状态,如果当前线程成功获取同步状态,
*该方法直接返回,否则当前线程会被封装成一个节点放入同步队列中等待
*/
public final void acquireShared(int arg) {//现实省去...}
/*以acquireShared方法类似,在其基础上增加了响应中断的功能
*/
public final void acquireSharedInterruptibly(int arg){//现实省去...}
/*在acquireSharedInterruptibly方法的基础上增加了超时限制
*/
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout){//现实省去...}
/*独占模式释放同步状态,该方法会先调用重写的tryRelease方法,如果释放同步状态成功,将同步队列中第一个节点中
*对应线程唤醒
*/
public final boolean release(int arg) {//现实省去...}
/*共享模式释放同步状态,该方法会先调用重写的tryReleaseShared方法
*/
public final boolean releaseShared(int arg) {//现实省去...}
同步器提供的模板方法可分为二大类:独占模式获取与释放同步状态,共享模式获取与释放同步状态。
# ReentrantLock 默认采用非公平锁,除非你在构造方法中传入参数 true
public ReentrantLock() {
// 默认非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁的 lock 方法:
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
非公平锁的 lock 方法:
static final class NonfairSync extends Sync {
final void lock() {
// 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 这里没有对阻塞队列进行判断
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
总结:
公平锁和非公平锁只有两处不同:
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
CAS :
如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择;
流程图:
流程如下:
# 以独占模式获取锁并且忽略中断。实现锁,需要子类去重写tryAcquire方法。
# 我们看到,这个方法,如果tryAcquire(arg) 返回true, 也就结束了。
# 否则,acquireQueued方法会将线程压到队列中
public final void acquire(int arg) {
/* 首先调用tryAcquire(1)一下,从名字上就知道,这个只是试一试
因为有可能直接就成功了呢,也就不需要进队列排队了,
对于公平锁的语义就是:本来就没人持有锁,根本没必要进队列等待(又是挂起,又是等待被唤醒的) */
if (!tryAcquire(arg) &&
// tryAcquire(arg)没有成功,这个时候需要把当前线程挂起,放到阻塞队列中。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
# 通过tryAcquire 尝试获取锁,如果获取成功则直接返回。获取不到锁则调用acquireQueued方法,acquireQueued方法又以addWaiter返回值作为参数。
# acquire 流程:
# 1.调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
# 2.没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
# 3.acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
# 4.如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
tryAcquire 尝试获取锁方法:
# 尝试直接获取锁,返回值是boolean,代表是否获取到锁
# 返回true:1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// state == 0 此时此刻没有线程持有锁
if (c == 0) {
// 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,
// 看看有没有别人在队列中等了半天了
if (!hasQueuedPredecessors() &&
/* 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了 =_=
因为刚刚还没人的,我判断过了 */
compareAndSetState(0, acquires)) {
// 到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁
setExclusiveOwnerThread(current);
return true;
}
}
// 会进入这个else if分支,说明是重入了,需要操作:state=state+1
// 这里不存在并发问题
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
/* 如果到这里,说明前面的if和else if都没有返回true,说明没有获取到锁
回到上面一个外层调用方法继续看:
if (!tryAcquire(arg)
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); */
return false;
}
# 假设tryAcquire(arg) 返回false,那么代码将执行:acquireQueued(addWaiter(Node.EXCLUSIVE), arg),
# 这个方法,首先需要执行:addWaiter(Node.EXCLUSIVE)
addWaiter 添加到队列方法:
# 此方法的作用是把线程包装成node,同时进入到队列中
private Node addWaiter(Node mode) {
// 生成一个当前线程的节点(mode为Node.EXCLUSIVE时表示独占、为Node.SHARED时共享模式)
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速方式直接放到队尾。
// 以下几行代码想把当前node加到链表的最后面去,也就是进到阻塞队列的最后
Node pred = tail;
if (pred != null) {
// 尾节点存在时,将当前的队尾节点,设置为自己的前驱
// (这个尾节点就是我自己节点的前面一个节点,而我就是这个尾节点的后面一个节点,用链表的形式连接)
node.prev = pred;
// 用CAS把自己设置为队尾, 如果成功后,tail == node 了,这个节点成为阻塞队列新的尾巴
if (compareAndSetTail(pred, node)) {
// 进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,
// 上面已经有 node.prev = pred,加上下面这句,也就实现了和之前的尾节点双向连接了
pred.next = node;
// 线程入队了,可以返回了
return node;
}
}
/* 上一步失败则通过enq入队。
仔细看看上面的代码,如果会到这里,
说明 pred==null(队列是空的) 或者 CAS失败(有线程在竞争入队)
enq方法将node作为尾节点添加到同步队列(同时该方法也是条件队列节点转换成同步队列节点的方法)*/
enq(node);
return node;
}
enq 尾点添加同步队列、条件队列节点转换为同步队列方法:
# 采用自旋的方式入队
# 到这个方法只有两种可能:等待队列为空,或者有线程竞争入队,
# 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 队列为空也会进来这里,若尾节点不存在进if语句里面执行
if (t == null) { // Must initialize
/* 初始化head节点
原来 head 和 tail 初始化的时候都是 null 的
还是一步CAS,现在可能是很多线程同时进来,生成一个默认的节点作为头节点 */
if (compareAndSetHead(new Node()))
/* 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道
这个时候有了head,但是tail还是null,设置一下,
把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了
注意:这里只是设置了tail=head,这里没有return
所以,设置完了以后,继续for循环,下次就到下面的else分支了 */
tail = head;
} else {
/* 下面几行,和上一个方法 addWaiter 是一样的,
只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上重复排
node节点的前驱节点指向尾节点 */
node.prev = t;
// CAS操作将尾节点指向node节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued 队列获取锁:
通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了,然后进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了,和医院排队拿号有点相似
# 现在,又回到这段代码了
# if (!tryAcquire(arg)
# && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
# selfInterrupt();
#
# 下面这个方法,参数node,经过addWaiter(Node.EXCLUSIVE),此时已经进入阻塞队列
# 注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,
# 意味着上面这段代码将进入selfInterrupt(),所以正常情况下,下面应该返回false
# 这个方法非常重要,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //标记是否成功拿到资源
try {
boolean interrupted = false; //标记等待过程中是否被中断过
for (;;) {
final Node p = node.predecessor();
// p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head
// 注意,阻塞队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为阻塞队列
// 所以当前节点可以去试抢一下锁
// 这里我们说一下,为什么可以去试试:
// 首先,它是队头,这个是第一个条件,其次,当前的head有可能是刚刚初始化的node,
// enq(node) 方法里面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程
// 也就是说,当前的head不属于任何一个线程,所以作为队头,可以去试一试,
// tryAcquire已经分析过了, 忘记了请往前看一下,就是简单用CAS试操作一下state
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 到这里,说明上面的if分支没有成功,要么当前node本来就不是队头,
// 要么就是tryAcquire(arg)没有抢赢别人,继续往下看
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 什么时候 failed 会为 true???
// tryAcquire() 方法抛异常的情况
if (failed)
cancelAcquire(node);
}
}
# acquireQueued 流程:
# 1.结点进入队尾后,检查状态,找到安全休息点;
# 2.调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
# 3.被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。
shouldParkAfterFailedAcquire 判断线程是否需要阻塞:
# 这个方法说的是:"当前线程没有抢到锁,是否需要挂起当前线程?"
# 第一个参数是前驱节点,第二个参数才是代表当前线程的节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //拿到前驱的状态
// 前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 前驱节点 waitStatus大于0 ,之前说过,大于0 说明前驱节点取消了排队。
// 这里需要知道这点:进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的。
// 所以下面这块代码说的是将当前节点的prev指向waitStatus<=0的节点,
// 简单说,就是为了找个好爹,因为你还得依赖它来唤醒呢,如果前驱节点取消了排队,
// 找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 仔细想想,如果进入到这个分支意味着什么
// 前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3
// 在我们前面的源码中,都没有看到有设置waitStatus的,所以每个新的node入队时,waitStatu都是0
// 正常情况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0
// 用CAS将前驱节点的waitStatus设置为Node.SIGNAL(也就是-1)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 这个方法返回 false,那么会再走一次 for 循序,
// 然后再次进来此方法,此时会从第一个分支返回 true
return false;
}
# private static boolean shouldParkAfterFailedAcquire(Node pred, Node node)
# 这个方法结束根据返回值我们简单分析下:
# 如果返回true, 说明前驱节点的waitStatus==-1,是正常情况,那么当前线程需要被挂起,等待以后被唤醒
# 我们也说过,以后是被前驱节点唤醒,就等着前驱节点拿到锁,然后释放锁的时候叫你好了
# 如果返回false, 说明当前不需要被挂起,为什么呢?往后看
# 跳回到前面是这个方法
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
# 1. 如果shouldParkAfterFailedAcquire(p, node)返回true,那么需要执行parkAndCheckInterrupt():
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 调用park()使线程进入waiting状态
return Thread.interrupted(); // 如果被唤醒,查看自己是不是被中断的。
}
# 又回到这个方法了:acquireQueued(final Node node, int arg),这个时候,node的前驱是head了
release 释放锁:
# AQS 中的释放锁方法
# 此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。
# 这也正是unlock()的语义,当然不仅仅只限于unlock()。
public final boolean release(int arg) {
if (tryRelease(arg)) {
//释放成功
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒头节点的后继节点
unparkSuccessor(h);
return true;
}
return false;
}
# 它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了.
# 所以自定义同步器在设计tryRelease()的时候要明确这一点.
# ReentrantLock 中的 tryRelease方法:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否完全释放锁
boolean free = false;
// 其实就是重入的问题,如果c==0,也就是说没有嵌套锁了,可以释放了,否则还不能释放掉
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor 唤醒后继节点(唤醒等待队列中最前边的那个未放弃线程):
# 从上面调用处知道,参数node是head头结点
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
// 如果head节点当前waitStatus<0, 将其修改为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 下面的代码就是唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1)
// 从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的
Node s = node.next;
// 当node的后继节点为null或者状态为cancelled时从尾节点向前遍历,找到最先入列且状态不是cancelled的节
if (s == null || s.waitStatus > 0) {
s = null;
// 从后往前找,不必担心中间有节点取消(waitStatus==1)的情况
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒线程
LockSupport.unpark(s.thread);
}
# 唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后
# 进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。
# 这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,
# 下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了
总结:
回顾环节
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;
}
}
}
}
# 独占模式下保证线程同步的操作是这样的,将state设为0,当某个线程要独占时都需要先判断state是不是0,如果不是就进入阻塞状态等待,
# 如果是0就把state设置为1。然后进行操作,实现这个操作需要设置3种操作:分别是尝试获取同步状态、释放同步状态、判断是否有线程独占。
# 这三种操作分别对应下面三个方法tryAcquire(tryAcquireNanos方法加了超时限制,超时就自动视为获取失败)、tryRelease、isHeldExclusively。
public class PlainLock {
private static class Sync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
// TODO Auto-generated method stub
return compareAndSetState(0, 1);
}
@Override
protected boolean tryRelease(int arg) {
// TODO Auto-generated method stub
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
// TODO Auto-generated method stub
return getState() == 1;
}
}
private Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
//保证不超过两个线程同时访问的锁
public class DoubleLock {
private class Sync extends AbstractQueuedSynchronizer{
public Sync() {
super();
setState(2);
}
//共享模式下需要一开始在构造方法里设置state的值
//然后在重写tryAcquireShared和tryReleaseShared方法
//该返回值大于等于0说明获取同步状态成功,否则加入同步队列
@Override
protected int tryAcquireShared(int arg) {
// TODO Auto-generated method stub
while(true) {
int now = getState();
int next = getState() - arg;
if(compareAndSetState(now, next)) {
return next;
}
}
}
@Override
protected boolean tryReleaseShared(int arg) {
// TODO Auto-generated method stub
while(true) {
int now = getState();
int next = getState() + arg;
if(compareAndSetState(now, next)) {
return true;
}
}
}
}
private Sync sy = new Sync();
public void lock() {
sy.acquireShared(1);
}
public void unlock() {
sy.releaseShared(1);
}
}
class Mutex implements Lock, java.io.Serializable {
// 自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 判断是否锁定状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获取资源,立即返回。成功则返回true,否则false。
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 这里限定只能为1个量
if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
return true;
}
return false;
}
// 尝试释放资源,立即返回。成功则为true,否则false。
protected boolean tryRelease(int releases) {
assert releases == 1; // 限定为1个量
if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);//释放资源,放弃占有状态
return true;
}
}
// 真正同步类的实现都依赖继承于AQS的自定义同步器!
private final Sync sync = new Sync();
//lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
public void lock() {
sync.acquire(1);
}
//tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
public boolean tryLock() {
return sync.tryAcquire(1);
}
//unlock<-->release。两者语文一样:释放资源。
public void unlock() {
sync.release(1);
}
//锁是否占有状态
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
使用总结:
公共方法 |
子类需要自定义的方法(AQS中默认返回异常,子类覆盖实现) |
子类可直接使用的方法 |
|
独占模式 | CAS操作节点、state同步状态 compareAndSetState 设置同步状态compareAndSetHead 设置head节点
|
protected boolean tryAcquire(int arg)获取资源 protected boolean tryRelease(int arg) 释放资源 protected boolean isHeldExclusively()该线程是否正在独占资源。 |
AbstractOwnableSynchronizer是AQS的父类,继承AQS类自然继承了AbstractOwnableSynchronizer, 方法: protected final Thread getExclusiveOwnerThread()获取当前独占线程 |
共享模式 | protected int tryAcquireShared(int arg)获取资源 protected boolean tryReleaseShared(int arg) 释放资源 |
1.独占模式:
ReentrantLock:可重入锁。state=0独占锁,或者同一线程可多次获取锁(获取+1,释放-1)。
Worker(java.util.concurrent.ThreadPoolExecutor类中的内部类)线程池类。shutdown关闭空闲工作线程,中断worker工作线程是独占的,互斥的。
2.共享模式:
Semaphore:信号量。 控制同时有多少个线程可以进入代码段。(互斥锁的拓展)
CountDownLatch:倒计时器。 初始化一个值,多线程减少这个值,直到为0,倒计时完毕,执行后续代码。
3.独占+共享模式:
ReentrantReadWriteLock:可重入读写锁。独占写+共享读,即并发读,互斥写。
3.nextWaiter介绍
Node 的属性:
volatile int waitStatus; # 可取值 0、CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
prev 和 next 用于实现阻塞队列的双向链表,这里的 nextWaiter 用于实现条件队列的单向链表
nextWaiter 为条件队列,用 ConditionObject 来描述条件队列节点。(AQS将条件队列和同步队列分开了)
Condition ,即条件(也称为条件队列或条件变量)。它主要是为了在 JUC 框架中提供和 Java 传统的监视器风格的 wait、notify、notifyAll 方法类似的功能。
Condition 自己也维护了一个队列,该队列的作用是维护一个等待 signal 信号的队列,两个队列的作用是不同,事实上,每个线程也仅仅会同时存在以上两个队列中的一个。将设有线程 1、2,下面来具体过程。
线程1:
线程1 调用 reentrantLock.lock时,持有锁。
线程1 调用 await 方法,进入[条件等待队列],同时释放锁。
线程1 获取到线程2 signal 信号,从 [条件等待队列] 进入 [同步等待队列]。
线程2:
线程2 调用 reentrantLock.lock时,由于锁被线程1 持有,进入 [同步等待队列]。
由于线程1 释放锁,线程2 从 [同步等待队列] 移除,获取到锁。线程2 调用 signal 方法,导致线程 1 被唤醒。
线程2 调用 reentrantLock.unlock 。线程1 获取锁,继续循环。
条件等待队列,指的是 Condition 内部自己维护的一个队列,不同于 AQS 的[同步等待队列]。它具有以下特点:
条件队列(单向链表)节点会在转成同步队列节点时将waitStatus从CONDITION变为0,并且会添加具体指向next和prev的引用通过 Node 的 nextWaiter 来指向下一个节点。
condition1.await()
方法即可将当前线程 1 包装成 Node 后加入到条件队列中,然后阻塞在这里,不继续往下执行,条件队列是一个单向链表;condition1.signal()
触发一次唤醒,此时唤醒的是队头,会将condition1 对应的条件队列的 firstWaiter(队头) 移到阻塞队列的队尾,等待获取锁,获取锁后 await 方法才能返回,继续往下执行。上面的 2->3->4 描述了一个最简单的流程,没有考虑中断、signalAll、还有带有超时参数的 await 方法等
await 阻塞
# 首先,这个方法是可被中断的,不可被中断的是另一个方法 awaitUninterruptibly()
# 这个方法会阻塞,直到调用 signal 方法(指 signal() 和 signalAll(),下同),或被中断
public final void await() throws InterruptedException {
// 既然该方法要响应中断,那么在最开始就判断中断状态
if (Thread.interrupted())
throw new InterruptedException();
// 创建包含当前线程的节点并添加到[条件等待队列] condition
Node node = addConditionWaiter();
// 释放锁,返回值是释放锁之前的 state 值
// await() 之前,当前线程是必须持有锁的,这里肯定要释放掉
// 失败则中断线程,并将节点的状态置为 CANCELD
int savedState = fullyRelease(node);
int interruptMode = 0;
# 判断该节点是否在[同步等待队列]
// 这里退出循环有两种情况
// 1. isOnSyncQueue(node) 返回 true,即当前 node 已经转移到阻塞队列了
// 2. checkInterruptWhileWaiting(node) != 0 会到 break,然后退出循环,代表的是线程中断
while (!isOnSyncQueue(node)) {
// 不在的话,则线程进入阻塞状态
LockSupport.park(this);
// 表示线程被唤醒的操作:确定中断模式并退出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 被唤醒后,将进入阻塞队列,等待获取锁
// 成功获取独占锁后,并判断 interruptMode 的值
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
// 清除条件等待队列上节点状态不为 CONDITION 的节点
unlinkCancelledWaiters();
if (interruptMode != 0)
// 根据 interruptMode 作出对应的动作
// 若为 THROW_IE 则抛出异常中断线程
// 若为 REINTERRUPT 则设置线程中断标记位
reportInterruptAfterWait(interruptMode);
}
# awaitUninterruptibly 为不抛出 InterruptedException 的 await
addConditionWaiter 是将当前节点加入到条件队列,这种条件队列内的操作是线程安全的。
// 将当前线程对应的节点入队,插入队尾
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果条件队列的最后一个节点取消了,将其清除出去
// 为什么这里把 waitStatus 不等于 Node.CONDITION,就判定为该节点发生了取消排队?
if (t != null && t.waitStatus != Node.CONDITION) {
// 这个方法会遍历整个条件队列,然后会将已取消的所有节点清除出队列
unlinkCancelledWaiters();
t = lastWaiter;
}
// node 在初始化的时候,指定 waitStatus 为 Node.CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// t 此时是 lastWaiter,队尾
// 如果队列为空
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
# 这块代码很简单,就是将当前线程进入到条件队列的队尾。
unlinkCancelledWaiters 该方法用于清除队列中已经取消等待的节点。
# 当 await 的时候如果发生了取消操作,或者是在节点入队的时候,发现最后一个节点是被取消的,会调用一次这个方法。
# 等待队列是一个单向链表,遍历链表将已经取消等待的节点清除出去
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 如果节点的状态不是 Node.CONDITION 的话,这个节点就是被取消的
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
fullyRelease 完全释放独占锁 该方法表示释放独占锁的所有重入计数
回到 wait 方法,节点入队了以后,会调用 int savedState = fullyRelease(node);
方法释放锁,注意,这里是完全释放独占锁
(fully release),因为 ReentrantLock 是可以重入的。
考虑一下这里的 savedState。如果在 condition1.await() 之前,假设线程先执行了 2 次 lock() 操作,那么 state 为 2,
我们理解为该线程持有 2 把锁,这里 await() 方法必须将 state 设置为 0,然后再进入挂起状态,这样其他线程才能持有锁。
当它被唤醒的时候,它需要重新持有 2 把锁,才能继续下去。
# 首先,我们要先观察到返回值 savedState 代表 release 之前的 state 值
# 对于最简单的操作:先 lock.lock(),然后 condition1.await()。
# 那么 state 经过这个方法由 1 变为 0,锁释放,此方法返回 1
# 相应的,如果 lock 重入了 n 次,savedState == n
# 如果这个方法失败,会将节点设置为"取消"状态,并抛出异常 IllegalMonitorStateException
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
// 这里使用了当前的 state 作为 release 的参数,也就是完全释放掉锁,将 state 置为 0
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
考虑一下,如果一个线程在不持有 lock 的基础上,就去调用 condition1.await() 方法,它能进入条件队列,但是在上面的这个方法中,
由于它不持有锁,release(savedState) 这个方法肯定要返回 false,进入到异常分支,然后进入 finally 块设置
node.waitStatus = Node.CANCELLED,这个已经入队的节点之后会被后继的节点”请出去“。
释放掉锁以后,接下来是这段,这边会自旋,如果发现自己还没到阻塞队列,那么挂起,等待被转移到阻塞队列。
int interruptMode = 0;
// 如果不在阻塞队列中,注意了,是阻塞队列
while (!isOnSyncQueue(node)) {
// 线程挂起
LockSupport.park(this);
// 这里可以先不用看了,等看到它什么时候被 unpark 再说
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
isOnSyncQueue(Node node) 于判断节点是否已经转移到阻塞队列了
# 在节点入条件队列的时候,初始化时设置了 waitStatus = Node.CONDITION
# 前面提到,signal 的时候需要将节点从条件队列移到阻塞队列,
# 这个方法就是判断 node 是否已经移动到阻塞队列了
final boolean isOnSyncQueue(Node node) {
// 当 node 的状态为 CONDITION 或同步等待队列为空,则返回 false
// 移动过去的时候,node 的 waitStatus 会置为 0,signal 方法的时候会说到
// 如果 waitStatus 还是 Node.CONDITION,也就是 -2,那肯定就是还在条件队列中
// 如果 node 的前驱 prev 指向还是 null,说明肯定没有在 阻塞队列(prev是阻塞队列链表中使用的)
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 后继节点不为空,说明该节点肯定在同步等待队列中
// 如果 node 已经有后继节点 next 的时候,那肯定是在阻塞队列了
if (node.next != null)
return true;
// 下面这个方法从阻塞队列的队尾开始从后往前遍历找,如果找到相等的,说明在阻塞队列,否则就是不在阻塞队列
// 可以通过判断 node.prev() != null 来推断出 node 在阻塞队列吗?答案是:不能。
// 这个可以看上篇 AQS 的入队方法,首先设置的是 node.prev 指向 tail,
// 然后是 CAS 操作将自己设置为新的 tail,可是这次的 CAS 是可能失败的。
return findNodeFromTail(node);
}
// 从阻塞队列的队尾往前遍历,如果找到,返回 true
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
为了大家理解,这里我们先看唤醒操作,因为刚刚到 LockSupport.park(this);
把线程挂起了,等待唤醒。
唤醒操作通常由另一个线程来操作,就像生产者-消费者模式中,如果线程因为等待消费而挂起,那么当生产者生产了一个东西后,会调用 signal 唤醒正在等待的线程来消费。
// 唤醒等待了最久的线程
// 其实就是,将这个线程对应的 node 从条件队列转移到阻塞队列
public final void signal() {
// 调用 signal 方法的线程必须持有当前的独占锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
// 从条件队列队头往后遍历,找出第一个需要转移的 node
// 因为前面我们说过,有些线程会取消排队,但是可能还在队列中
private void doSignal(Node first) {
do {
// 将 firstWaiter 指向 first 节点后面的第一个,因为 first 节点马上要离开了
// 如果将 first 移除后,后面没有节点在等待了,那么需要将 lastWaiter 置为 null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 因为 first 马上要被移到阻塞队列了,和条件队列的链接关系在这里断掉
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
// 这里 while 循环,如果 first 转移不成功,那么选择 first 后面的第一个节点进行转移,依此类推
}
// 将节点从条件队列转移到阻塞队列
// true 代表成功转移
// false 代表在 signal 之前,节点已经取消了
final boolean transferForSignal(Node node) {
// CAS 如果失败,说明此 node 的 waitStatus 已不是 Node.CONDITION,说明节点已经取消,
// 既然已经取消,也就不需要转移了,方法返回,转移后面一个节点
// 否则,将 waitStatus 置为 0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// enq(node): 自旋进入阻塞队列的队尾
// 注意,这里的返回值 p 是 node 在阻塞队列的前驱节点
Node p = enq(node);
int ws = p.waitStatus;
// ws > 0 说明 node 在阻塞队列中的前驱节点取消了等待锁,直接唤醒 node 对应的线程。唤醒之后会怎么样,后面再解释
// 如果 ws <= 0, 那么 compareAndSetWaitStatus 将会被调用,上篇介绍的时候说过,节点入队后,需要把前驱节点的状态设为 Node.SIGNAL(-1)
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果前驱节点取消或者 CAS 失败,会进到这里唤醒线程,之后的操作看下一节
LockSupport.unpark(node.thread);
return true;
}
# 正常情况下,ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL) 这句中,ws <= 0,
# 而且 compareAndSetWaitStatus(p, ws, Node.SIGNAL) 会返回 true,所以一般也不会进去 if 语句块中唤醒 node 对应的线程。
# 然后这个方法返回 true,也就意味着 signal 方法结束了,节点进入了阻塞队列。
# 假设发生了阻塞队列中的前驱节点取消等待,或者 CAS 失败,只要唤醒线程,让其进到下一步即可。
checkInterruptWhileWaiting(node) 此方法用于判断是否在线程挂起期间发生了中断
上一步 signal 之后,我们的线程由条件队列转移到了阻塞队列,之后就准备获取锁了。只要重新获取到锁了以后,继续往下执行。
等线程从挂起中恢复过来
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 线程挂起
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
interruptMode 可以取值为 REINTERRUPT(1),THROW_IE(-1),0
有以下三种情况会让 LockSupport.park(this); 这句返回继续往下执行:
线程唤醒后第一步是调用 checkInterruptWhileWaiting(node) 这个方法,此方法用于判断是否在线程挂起期间发生了中断,如果发生了中断,是 signal 调用之前中断的,还是 signal 之后发生的中断。
// 1. 如果在 signal 之前已经中断,返回 THROW_IE
// 2. 如果是 signal 之后中断,返回 REINTERRUPT
// 3. 没有发生中断,返回 0
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
# Thread.interrupted():如果当前线程已经处于中断状态,那么该方法返回 true,同时将中断状态重置为 false
# 所以,才有后续的 重新中断(REINTERRUPT) 的使用。
怎么判断是 signal 之前还是之后发生的中断:
# 只有线程处于中断状态,才会调用此方法
# 如果需要的话,将这个已经取消等待的节点转移到阻塞队列
# 返回 true:如果此线程在 signal 之前被取消
final boolean transferAfterCancelledWait(Node node) {
// 用 CAS 将节点状态设置为 0
// 如果这步 CAS 成功,说明是 signal 方法之前发生的中断,因为如果 signal 先发生的话,signal 中会将 waitStatus 设置为 0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 将节点放入阻塞队列
// 这里我们看到,即使中断了,依然会转移到阻塞队列
enq(node);
return true;
}
// 到这里是因为 CAS 失败,肯定是因为 signal 方法已经将 waitStatus 设置为了 0
// signal 方法会将节点转移到阻塞队列,但是可能还没完成,这边自旋等待其完成
// 当然,这种事情还是比较少的吧:signal 调用之后,没完成转移之前,发生了中断
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
# 即使发生了中断,节点依然会转移到阻塞队列。
while 循环怎么退出:
要么中断,要么转移成功。
这里描绘了一个场景,本来有个线程,它是排在条件队列的后面的,但是因为它被中断了,那么它会被唤醒,然后它发现自己不是被 signal 的那个,但是它会自己主动去进入到阻塞队列。
while 循环出来以后,下面是这段代码:
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
由于 while 出来后,我们确定节点已经进入了阻塞队列,准备获取锁。
这里的 acquireQueued(node, savedState) 的第一个参数 node 之前已经经过 enq(node) 进入了队列,参数 savedState 是之前释放锁前的 state,这个方法返回的时候,代表当前线程获取了锁,而且 state == savedState了。
注意,前面我们说过,不管有没有发生中断,都会进入到阻塞队列,而 acquireQueued(node, savedState) 的返回值就是代表线程是否被中断。如果返回 true,说明被中断了,而且 interruptMode != THROW_IE,说明在 signal 之前就发生中断了,这里将 interruptMode 设置为 REINTERRUPT,用于待会重新中断。
继续往下:
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
# node.nextWaiter != null 怎么满足。signal 的时候会将节点转移到阻塞队列,有一步是 node.nextWaiter = null,将断开节点和条件队列的联系。
在判断发生中断的情况下,是 signal 之前还是之后发生的?
如果 signal 之前就中断了,也需要将节点进行转移到阻塞队列,这部分转移的时候,是没有设置 node.nextWaiter = null 的。
如果有节点取消,也会调用 unlinkCancelledWaiters() 这个方法,就是这里了。
interruptMode:
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
# 经过7 步,执行完毕
public final long awaitNanos(long nanosTimeout)
throws InterruptedException
public final boolean awaitUntil(Date deadline)
throws InterruptedException
public final boolean await(long time, TimeUnit unit)
throws InterruptedException
这三个方法都差不多,挑一个出来:
# 超时的思路还是很简单的,不带超时参数的 await 是 park,然后等待别人唤醒。而现在就是调用 parkNanos 方法来休眠指定的时间,
# 醒来后判断是否 signal 调用了,调用了就是没有超时,否则就是超时了。超时的话,自己来进行转移到阻塞队列,然后抢锁。
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
// 等待这么多纳秒
long nanosTimeout = unit.toNanos(time);
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
// 当前时间 + 等待时长 = 过期时间
final long deadline = System.nanoTime() + nanosTimeout;
// 用于返回 await 是否超时
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 时间到啦
if (nanosTimeout <= 0L) {
// 这里因为要 break 取消等待了。取消等待的话一定要调用 transferAfterCancelledWait(node) 这个方法
// 如果这个方法返回 true,在这个方法内,将节点转移到阻塞队列成功
// 返回 false 的话,说明 signal 已经发生,signal 方法将节点转移了。也就是说没有超时嘛
timedout = transferAfterCancelledWait(node);
break;
}
// spinForTimeoutThreshold 的值是 1000 纳秒,也就是 1 毫秒
// 也就是说,如果不到 1 毫秒了,那就不要选择 parkNanos 了,自旋的性能反而更好
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
// 得到剩余时间
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return !timedout;
}
怎么取消对锁的竞争,最重要的方法是这个:
# 首先,到这个方法的时候,节点一定是入队成功的。
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; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
# parkAndCheckInterrupt()方法:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
如果我们要取消一个线程的排队,我们需要在另外一个线程中对其进行中断。比如某线程调用 lock() 老久不返回,我想中断它。一旦对其进行中断,此线程会从 LockSupport.park(this);
中唤醒,然后 Thread.interrupted();
返回 true。
我们发现一个问题,即使是中断唤醒了这个线程,也就只是设置了 interrupted = true
然后继续下一次循环。而且,由于 Thread.interrupted();
会清除中断状态,第二次进 parkAndCheckInterrupt 的时候,返回会是 false。
所以,我们要看到,在这个方法中,interrupted 只是用来记录是否发生了中断,然后用于方法返回值,其他没有做任何相关事情。
# 外层方法怎么处理 acquireQueued 返回 false 的情况。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
# 所以说,lock() 方法处理中断的方法就是,你中断归中断,我抢锁还是照样抢锁,几乎没关系,只是我抢到锁了以后,
# 设置线程的中断状态而已,也不抛出任何异常出来。调用者获取锁后,可以去检查是否发生过中断,也可以不理会。
我们来看 ReentrantLock 的另一个 lock 方法:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
# 方法上多了个 throws InterruptedException
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(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();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 就是这里了,一旦异常,马上结束这个方法,抛出异常。
// 这里不再只是标记这个方法的返回值代表中断状态
// 而是直接抛出异常,而且外层也不捕获,一直往外抛到 lockInterruptibly
throw new InterruptedException();
}
} finally {
// 如果通过 InterruptedException 异常出去,那么 failed 就是 true 了
if (failed)
cancelAcquire(node);
}
}
# 继续 cancelAcquire(Node node)
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
// 找一个合适的前驱。其实就是将它前面的队列中已经取消的节点都”请出去“
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
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)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
一行行看下去就是了,节点取消,只要把 waitStatus 设置为 Node.CANCELLED,会有非常多的情况被从阻塞队列中请出去,主动或被动。
Java 中的中断和操作系统的中断还不一样,这里就按照状态来理解吧
中断不是类似 linux 里面的命令 kill -9 pid,不是说我们中断某个线程,这个线程就停止运行了。中断代表线程状态,每个线程都关联了一个中断状态,是一个 true 或 false 的 boolean 值,初始值为 false。
// Thread 类中的实例方法,持有线程实例引用即可检测线程中断状态
public boolean isInterrupted() {}
// Thread 中的静态方法,检测调用这个方法的线程是否已经中断
// 注意:这个方法返回中断状态的同时,会将此线程的中断状态重置为 false
// 所以,如果我们连续调用两次这个方法的话,第二次的返回值肯定就是 false 了
public static boolean interrupted() {}
// Thread 类中的实例方法,用于设置一个线程的中断状态为 true
public void interrupt() {}
我们说中断一个线程,其实就是设置了线程的 interrupted status 为 true,至于说被中断的线程怎么处理这个状态,那是那个线程自己的事。如以下代码:
while (!Thread.interrupted()) {
doWork();
System.out.println("我做完一件事了,准备做下一件,如果没有其他线程中断我的话");
}
这种代码就是会响应中断的,它会在干活的时候先判断下中断状态,不过,除了 JDK 源码外,其他用中断的场景还是比较少的,毕竟 JDK 源码非常讲究。
当然,中断除了是线程状态外,还有其他含义,否则也不需要专门搞一个这个概念出来了。
如果线程处于以下三种情况,那么当线程被中断的时候,能自动感知到:
1.来自 Object 类的 wait()、wait(long)、wait(long, int),
来自 Thread 类的 join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)
这几个方法的相同之处是,方法上都有: throws InterruptedException
如果线程阻塞在这些方法上(我们知道,这些方法会让当前线程阻塞),这个时候如果其他线程对这个线程进行了中断,那么这个线程会从这些方法中立即返回,抛出 InterruptedException 异常,同时重置中断状态为 false。
2.实现了 InterruptibleChannel 接口的类中的一些 I/O 阻塞操作,如 DatagramChannel 中的 connect 方法和 receive 方法等
如果线程阻塞在这里,中断线程会导致这些方法抛出 ClosedByInterruptException 并重置中断状态。
3.Selector 中的 select 方法
一旦中断,方法立即返回
对于以上 3 种情况是最特殊的,因为他们能自动感知到中断(这里说自动,当然也是基于底层实现),并且在做出相应的操作后都会重置中断状态为 false。
注意: 如果线程阻塞在 LockSupport.park(Object obj) 方法,也叫挂起,这个时候的中断也会导致线程唤醒,但是唤醒后不会重置中断状态,所以唤醒后去检测中断状态将是 true。
停止线程方式:
1、正常退出
当线程中run()或者call()按照逻辑流程正常的执行结束了,线程也就自然停止了。
2、stop暴力停止
直接在程序中使用thread.stop(),线程会马上停止,但是可能导致数据不同步,或者资源得不到回收的问题而且stop已经标注为作废方法,所以使用一定要慎重。
3、interrupt()异常法。
我们经常会这么写代码:
# 这个代码的问题在于,我们将这个异常信息吞掉了
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// ignore
}
AQS 的做法很值得我们借鉴,我们知道 ReentrantLock 有两种 lock 方法:
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
前面我们提到过,lock() 方法不响应中断。如果 thread1 调用了 lock() 方法,过了很久还没抢到锁,这个时候 thread2 对其进行了中断,thread1 是不响应这个请求的,它会继续抢锁,当然它不会把“被中断”这个信息扔掉。我们可以看以下代码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 我们看到,这里也没做任何特殊处理,就是记录下来中断状态。
// 这样,如果外层方法需要去检测的时候,至少我们没有把这个信息丢了
selfInterrupt();// Thread.currentThread().interrupt();
}
而对于 lockInterruptibly() 方法,因为其方法上面有 throws InterruptedException
,这个信号告诉我们,如果我们要取消线程抢锁,直接中断这个线程即可,它会立即返回,抛出 InterruptedException 异常。
在并发包中,有非常多的这种处理中断的例子,提供两个方法,分别为响应中断和不响应中断,对于不响应中断的方法,记录中断而不是丢失这个信息。如 Condition 中的两个方法就是这样的:
void await() throws InterruptedException;
void awaitUninterruptibly();
通常,如果方法会抛出 InterruptedException 异常,往往方法体的第一句就是:
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); ...... }
interrupt()方法仅仅是在当前线程中打了一个停止的标识将中断标志修改为true,并没有真正的停止线程。如果在此基础上进入堵塞状态(sleep(),wait(),join()),马上就会抛出一个InterruptedException,且中断标志被清除,重新设置为false,线程退出。所以退出线程的方式不仅仅是sleep()+interrupt()方法,其它的暂停线程的方式都可以。
interrupt简述
this.interrupted()
测试当前线程是否已经中断(静态方法)。返回的是上一次的中断状态,并且会清除该状态,所以连续调用两次,第一次返回true,第二次返回false。
this.isInterrupted()
测试线程当前是否已经中断,但是不能清除状态标识。
问题1: Thread.interrupt()方法和InterruptedException异常的关系?是由interrupt触发产生了InterruptedException异常?
问题2:Thread.interrupt()会中断线程什么状态的工作? RUNNING or BLOCKING?
问题3: 一般Thread编程需要关注interrupt中断不?一般怎么处理?可以用来做什么?
问题4: LockSupport.park()和unpark(),与object.wait()和notify()的区别?
答:
问题5: LockSupport.park(Object blocker)传递的blocker对象做什么用?
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker); // 设置Thread.parkBlocker属性的值
unsafe.park(false, 0L);
setBlocker(t, null); // 清除Thread.parkBlocker属性的值
}
问题6: LockSupport能响应Thread.interrupt()事件不?会抛出InterruptedException异常?