- acquire时获取锁,失败则进入队列等待。成功则自动向下执行
- release时释放锁,自动触发唤醒下一个线程。 (基于LockSuport实现阻塞线程的唤醒以继续自旋竞争锁过程)
Java下整个Lock接口下实现的锁机制是通过AQS(这里我们将AbstractQueuedSynchronizer 或AbstractQueuedLongSynchronizer
统称为AQS)与Condition
来实现的。
基于AQS构建的Synchronizer包括ReentrantLock,Semaphore,CountDownLatch, ReetrantRead WriteLock,FutureTask等,这些Synchronizer实际上最基本的东西就是原子状态的获取和释放,只是条件不一样而已。
那下面我们就以AbstractQueuedSynchronizer
来进行讲解, 具体了解AQS的内部细节与实现原理。
抽象队列同步器AbstractQueuedSynchronizer (以下都简称AQS),是用来构建锁或者其他同步组件的基础框架,它具备两个核心功能:
AQS是一个抽象类,当我们要构建一个同步组件的时候,需要定义一个子类继承AQS,这里应用了模板方法设计模式
模板模式由一个抽象类和一个实现类组成,抽象类中主要有三类方法:
模板方法:实现了算法主体框架,供外部调用。里面会调用原语操作和钩子操作。
原语操作:即定义的抽象方法,子类必须重写。
钩子操作:和原语操作类似,也是供子类重写的,区别是钩子操作子类可以选择重写也可以选择不重写,如果不重写则使用抽象类默认操作,通常是一个空操作或抛出异常。
在AQS中没有原语操作,也就是说自定义的子类继承AQS后,不会强制子类重写任何方法。
AQS只提供了若干钩子操作,这些钩子操作的默认实现都是直接抛出异常。子类不需要重写所有的钩子操作,只需要根据要构建的同步组件的类型来决定要调用AQS中的哪些模板方法,再实现这些模板方法中用到了的钩子操作即可。
AQS中可供子类重写的钩子操作有:
方法名称 | 描述 |
---|---|
boolean tryAcquire(int arg) | 独占式尝试获取同步状态(通过CAS操作设置同步状态),如果成功返回true,反之返回false |
boolean tryRelease(int arg) | 独占式释放同步状态,成功返回true,失败返回false。 |
int tryAcquireShared(int arg) | 共享式的获取同步状态,返回大于等于0的值,表示获取成功,反之失败。 |
boolean tryReleaseShared(int arg) | 共享式释放同步状态,成功返回true,失败返回false。 |
boolean isHeldExclusively() | 判断同步器是否在独占模式下被占用,一般用来表示同步器是否被当前线程占用 |
可以看到可以重写的钩子操作既有独占式同步状态的获取与释放,也有共享式同步状态的获取与释放,这样就能支持构建不同类型的同步组件
譬如,ReentrantLock使用时同一时刻只有一个线程可以获得锁,因此就可以通过重写tryAcquire和tryRelease实现;而Semaphore在同一时刻可以有多个线程获得许可,因此就可以通过重写tryAcquireShared(int arg)和tryReleaseShared(int arg)实现,事实上,这两个同步组件正是这么实现的。
在子类重写钩子操作的时候,可以调用AQS中的以下方法来获取/设置AQS的同步状态state:
方法 | 描述 |
---|---|
int getState() | 获取当前同步状态 |
void setState(int newState) | 设置当前同步状态 |
boolean compareAndSetState(int expect, int update) | 使用CAS设置当前状态,保证状态更新的原子性 |
还会用到父类AbstractOwnableSynchronizer
提供的辅助函数
//当前锁的持有者线程
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
AQS将对外暴露一些模板方法用于同步状态的获取和设置,当然这些方法内部会调用其子类的模板方法
子类重写相关钩子操作后,AQS中提供的模板方法才能正常调用(如果模板方法中使用的钩子方法没有被子类重写,将抛出异常)
AQS中提供的模板方法有(这里列出了所有的模板方法,只挑了比较常用了写了描述,其他的可以自行查看源码注释):
方法 | 描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态(该方法会调用子类重写的tryAcquire(int arg))。 如果当前线程获取同步状态成功,则返回以便继续执行,否则进入同步队列的尾部等待,该方法会调用tryAcquire(int arg)方法。 |
void acquireInterruptibly(int arg) | 与 void acquire(int arg)基本逻辑相同,但是该方法响应中断。 如果当前没有获取到同步状态,那么就会进入等待队列,如果当前线程被中断(Thread().interrupt()),那么该方法将会抛出InterruptedException。并返回 |
boolean tryAcquireNanos(int arg, long nanosTimeout) | 在acquireInterruptibly(int arg)的基础上,增加了超时限制,如果在超时时间内获取到同步状态返回true,否则返回false |
boolean release(int arg) | 独占式释放同步状态,该方法会在释放同步状态后将第一个节点(对应刚刚释放同步状态的线程)的后继节点对应的线程唤醒。 |
方法 | 描述 |
---|---|
void acquireShared(int arg) | 共享式的获取同步状态(该方法会调用子类重写的tryAcquireShared(int arg)). 如果当前线程未获取到同步状态,将会进入同步队列的尾部等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态。 |
void acquireSharedInterruptibly(int arg) | 在acquireShared(int arg)的基本逻辑相同,增加了响应中断。 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 在acquireSharedInterruptibly的基础上,增加了超时限制。 |
boolean releaseShared(int arg) | 共享式的释放同步状态 |
方法 | 描述 |
---|---|
boolean hasQueuedThreads() | 此方法返回 Boolean 值表明是否有线程在等待获取此锁,根据head、tail |
boolean hasContended() | 判断是否有节点,无论是阻塞节点还是已获取到的节点 |
Thread getFirstQueuedThread() | 返回队列里第一个没有获取到锁的线程,如果head等于tail说明队列里没有线程在等待,直接返回null;否则,调用fullGetFirstQueuedThread。 |
boolean isQueued(Thread thread) | 判断线程是否在队列里(包含头节点了) |
boolean hasQueuedPredecessors() | 通过判断"当前线程"是不是在CLH队列的队首,来返回AQS中是不是有比“当前线程”等待更久的线程 |
int getQueueLength() | 此方法返回等待获取此锁的线程数量 |
Collection getQueuedThreads() | 返回一个 collection,它包含可能正等待获取此锁的线程 |
Collection getExclusiveQueuedThreads() | 返回包含可能正以独占模式等待获取的线程 collection |
Collection getSharedQueuedThreads() | 返回包含可能正以共享模式等待获取的线程 collection |
boolean owns(ConditionObject condition) | |
boolean hasWaiters(ConditionObject condition) | |
int getWaitQueueLength(ConditionObject condition) | 返回等待与此锁定相关的给定条件condition的线程估计数 |
Collection getWaitingThreads(ConditionObject condition) | 返回一个 collection,该 collection 包含那些正在与此同步器关联的给定条件上等待的线程。 |
这里我们借助一个例子来加深对AQS的理解,我们用AQS来自定义一个独占锁MutexLock(独占锁就是同一时刻只有一个线程可以获得锁,而其他线程只能被阻塞,一直到当前线程释放了锁,其他线程才有机会再获取锁):
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class MutexLock implements Lock{
private MutexSynchronizer synchronizer=new MutexSynchronizer();
@Override
public void lock() {
synchronizer.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
synchronizer.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return synchronizer.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return synchronizer.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
synchronizer.release(1);
}
@Override
public Condition newCondition() {
return synchronizer.newCondition();
}
}
我们让MutexLock实现了Lock接口,因此MutexLock就必须实现以上的方法,可以看到MutexLock中的方法本身都没有做任何操作,都是把请求委托给MutexSynchronizer的实例。
为了实现MutexLock中的方法,我们需要调用AQS的acquire、acquireInterruptibly、tryAcquire、tryAcquireNanos、release方法,这几个方法都是独占式获取、释放同步状态的方法
因此子类MutexSynchronizer需要重写和独占同步状态获取、释放相关的钩子操作:tryAcquire、tryRelease。
public class MutexLock implements Lock{
private static class MutexSynchronizer extends AbstractQueuedSynchronizer{
/**
* @param unused 这个参数是用来传同步状态的累加值的,因为我们实现的是独占锁,
* 因此这个参数实际用不到,我们在方法里累加值恒为1
*/
@Override
protected boolean tryAcquire(int unused) {
/**
* 用CAS来更新AQS的同步状态,如果原值是0则更新为1代表已经有线程获取了独占锁
*/
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread()); //设置当前独占锁的所有者线程
return true;
}
return false;
}
/**
* @param unused 这个参数是用来传同步状态的递减值的,因为我们实现的是独占锁,
* 因此这个参数实际用不到,我们在方法里递减值恒为1
*/
@Override
protected boolean tryRelease(int unused) {
//如果当前AQS同步状态是0,说明试图在没有获得同步状态的情况下释放同步状态,直接抛异常
if (getState()==0)
throw new IllegalMonitorStateException();
//这里不需要CAS而是直接把同步状态设置为0,因为我们实现的是独占锁,正常情况下会执行释放操作的线程只有同步状态的所有者线程
setState(0);
setExclusiveOwnerThread(null);
return true;
}
protected Condition newCondition() {
return new ConditionObject();
}
}
}
只需要以上的代码,我们就拥有了一个可以使用的独占锁。可以看到,需要我们自己写的主要就是tryAcquire()和tryRelease()这两个方法,其他的操作,如对获取锁失败线程的阻塞、唤醒,都是AQS替我们实现的。
当然,实际上Java已经帮我们实现了基本满足大部分需求的锁实现,具体的源码解析我们后续再说
我们前面介绍了,AQS使用一个int变量state表示同步状态,使用一个隐式的FIFO同步队列(隐式队列就是并没有声明这样一个队列,只是通过每个节点记录它的上个节点和下个节点来从逻辑上产生一个队列)来完成阻塞线程的排队。
在上文中我们提到AQS中主要通过一个FIFO(first-in-first-out)来控制线程的同步。
在实际程序中,AQS会将获取同步状态的线程构造成一个Node节点,并将该节点加入到队列中。如果该线程获取同步状态失败会阻塞该线程,当同步状态释放时,会把头节点中的线程唤醒,使其尝试获取同步状态。
下面我们就通过实际代码来了解Node节点中存储的信息。Node节点具体实现如下:
static final class Node {
//等待状态
volatile int waitStatus;
//当前转换为Node节点的线程。
volatile Thread thread;
//当前节点在同步队列中的上一个节点。
volatile Node prev;
//当前节点在同步队列中的下一个节点。
volatile Node next;
//Node既可以作为同步队列节点(竞争使用共享资源的线程)使用,也可以作为Condition的等待队列节点(类似与调用Obect.wait等待线程)使用(将会在后面讲Condition时讲到)。
//在作为同步队列节点时,nextWaiter可能有两个值:EXCLUSIVE、SHARED标识当前节点是独占模式还是共享模式;
//在作为等待队列节点使用时,nextWaiter保存后继节点。
Node nextWaiter;
}
等待状态主要包含以下状态
状态 | 值 | 含义 |
---|---|---|
CANCELLED | 1 | 被中断或获取同步状态超时的线程将会被置为该状态,且该状态下的线程不会再阻塞。 |
SIGNAL | -1 | 线程的后继线程正/已被阻塞,当该线程release或cancel时要重新这个后继线程(unpark) |
CONDITION | -2 | 标识当前节点是作为等待队列节点使用的。当前节点在Condition中的等待队列上,(关于Condition会在下篇文章进行介绍),其他线程调用了Condition的singal()方法后,该节点会从等待队列转移到AQS的同步队列中,等待获取同步锁。 |
PROPAGATE | -3 | 与共享式获取同步状态有关,该状态标识的节点对应线程处于可运行的状态。 |
0 | 0 | 初始状态 |
其中, SIGNAL是个很重要的概念:
队列拥有首节点和尾节点,这两个节点分别保存于AQS的两个字段:head、tail。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private transient volatile Node head;
private transient volatile Node tail;
private final boolean compareAndSetTail(Node expect, Node update) {
return U.compareAndSwapObject(this, TAIL, expect, update);
}
}
当一个线程想要获得同步状态的时候,如果当前有其他线程持有同步状态,当前线程将无法获取,转而被构造为一个Node添加到同步队列的尾部
而这个加入的过程必须保证线程安全,因此同步器提供了一个基于CAS的设置队尾的方法:compareAndSetTail(Node expect, Node update),它需要传递当前线程"认为"的队尾以作为CAS操作的比较。只有设置成功后,当前节点才正式与之前的尾节点建立关联。
需要额外注意的是:
在一个NewNode被CAS设置为队尾之前,这个NewNode的prev已经被指向之前的尾节点。而在这个Node被设置为队尾之后,之前尾节点的next才会被指向这个NewNode。
因此在任一时刻,从head向后遍历队列不一定能遍历到tail,因为最后的tail可能还没有被倒数第二个节点指为next,但是从tail向head遍历一定能遍历head。记住这个结论之后会用到。
在队列中,首节点是当前获取同步状态成功的节点。 首节点在释放同步状态时,会唤醒后继节点,而后继节点会在自己获取同步状态成功时,将自己设置为首节点
上图中,虚线部分为之前head指向的节点。
因为设置头节点是获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要CAS来进行保证,只需要将原头节点的next指向断开就行了。
接下来我们看下 AQS暴漏给外部调用的模板方法到底是如何实现的。
acquire(int arg)
独占式获取同步状态(该方法会调用子类重写的tryAcquire(int arg))。 如果当前线程获取同步状态成功,则返回以便继续执行,否则进入同步队列的尾部等待,该方法会调用tryAcquire(int arg)方法。
通过acquire(int arg)方法我们可以获取到同步状态,但是需要注意的是该方法并不会响应线程的中断与获取同步状态的超时机制。同时即使当前线程已经中断了,通过该方法放入的同步队列的Node节点(该线程构造的Node),也不会从同步队列中移除。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
在该方法中:
子类重写的方法tryAcquire(arg)
来获取同步状态acquireQueued()
的主要作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回接着我们查看 addWaiter(Node mode)
方法具体细节:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//将该线程构造成Node节点
Node pred = tail;
if (pred != null) {//尝试将尾指针 tail 指向当前线程构造的Node节点
node.prev = pred;
if (compareAndSetTail(pred, node)) {
//如果成功,那么将尾指针之前指向的节点的next指向 当前线程构造的Node节点
pred.next = node;
return node;
}
}
enq(node);//如果当前尾指针为null,则调用enq(final Node node)方法
return node;
}
//在enq(final Node node)方法中,通过死循环(你也可以叫做自旋)的方式来保证节点的正确的添加。
// 完整的设置尾节点方法
// 如果当前节点不为空,则把当前节点设为尾节点,并将原尾节点next指向当前节点;
// 如果当前尾节点为空,即当前同步队列为空,则新建一个傀儡节点作为首节点和尾节点,然后再将当前节点设为尾节点。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {//如果当前尾指针为null,那么尝试将头指针 head指向当前线程构造的Node节点
if (compareAndSetHead(new Node()))
tail = head;
} else {//如果当前尾指针(tail)不为null,那么尝试将尾指针 tail 指向当前线程构造的Node节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在该方法中,主要分为两个步骤:
接下来,我们继续查看acquireQueued(final Node node, int arg)
方法的处理。该方法才是整个多线程竞争同步状态的关键,大家一定要注意看!!!**,主要作用是
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {//【1】再次获取同步状态
final Node p = node.predecessor();//获取当前节点的上一节点
//如果上一节点是head锁指向的节点,且当前节点获取同步状态成功
if (p == head && tryAcquire(arg)) {
//设置head指向该节点,头结点就表示当前正占有锁资源的节点
setHead(node);
p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
failed = false; //表示锁资源成功获取,因此把failed置为false
return interrupted;
}
//【2】判断 当前这个获取同步状态失败的线程 是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//【阻塞并等待唤醒,同时判断当前线程是否已经中断了】
interrupted = true;
}
} finally {
if (failed)
//【3】如果线程中断了,那么就将该线程从同步队列中移除,同时唤醒下一节点
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//上一节点已经设置状态请求释放信号,因此当前节点可以安全地阻塞
return true;
if (ws > 0) {
//上一节点,已经被中断或者超时,那么接跳过所有状态为Node.CANCELLED继续往前寻找
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//其他状态,则调用cas操作设置 pred状态为Node.SINGAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//该方法在 shouldParkAfterFailedAcquire 方法返回true后执行,shouldParkAfterFailedAcquire 方法返回true代表前驱节点已经被设置为SIGNAL状态,
//因此当前节点可以阻塞等待唤醒了,使用LockSupport.park(this)方法来阻塞。
//这个方法会一直阻塞直到首节点唤醒当前节点或当前节点被中断,如果是被中断,中断标识将会被一直往上层方法传,最终acquire方法会执行selfInterrupt。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//通过LockSupport(在后面的文章中会具体介绍)的park来阻塞当前线程。
return Thread.interrupted();
}
仔细看看这个方法是个无限循环,感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,当然不会出现死循环,奥秘在于parkAndCheckInterrupt会把当前线程挂起,从而阻塞住线程的调用栈。
请求锁不成功的线程会被挂起在acquireQueued方法的第parkAndCheckInterrupt()
行,以后的代码必须等线程被解锁锁才能执行,假如被阻塞的线程得到解锁,则设置interrupted = true
,之后又进入无限循环, 再次开始竞争锁。
从无限循环的代码可以看出,并不是得到解锁的线程一定能获得锁,必须在第6行中调用tryAccquire重新竞争。非公平锁中有可能被新加入的线程获取到,从而导致刚刚被唤醒的线程再次阻塞;公平锁通过判断当前节点是否是头结点来保证锁的公平性。
在该方法中主要分为三个步骤(务必记得,头节点一定是持有同步状态的线程节点):
死循环(你也可以叫做自旋)
的方式来获取同步状态,如果当前节点的上一节点是head指向的节点且该节点获取同步状态成功
,那么会设置head指向该节点 ,同时将上一节点的next指向断开。如果当前节点的前驱是头节点,说明即将轮到自己获得同步状态,再次调用tryAcquire检查是否能获取到同步状态(这里之所以要再次检查,有两个原因:
一是因为尽管当前节点排到首节点后面,而且已经被首节点唤醒,但是首节点在唤醒当前节点后,并不是马上释放同步状态;
二是因为如果此时有新的线程第一次尝试获取同步状态正好赶在首节点释放同步状态,那么新的线程可能直接就不排队了直接获取到同步状态。)
shouldParkAfterFailedAcquire(Node pred, Node node)
方法来判断是需要否阻塞当前线程。如果该方法返回true,则调用parkAndCheckInterrupt()方法来阻塞线程,中止循环。如果该方法返回false,那么继续自旋循环,但是该方法内部会把当前节点的上一节点的状态修改为Node.SINGAL,以便下次循环时中止。
通过对独占式获取同步状态的理解,我们知道acquireQueued(final Node node, int arg)
方法中最终会执行finally语句块中的代码,来判断当前线程是否已经中断。如果中断,则通过那么cancelAcquire(Node node)
方法将该线程从同步队列中移除。
private void cancelAcquire(Node node) {
//如果当前节点已经不存在直接返回
if (node == null)return;
//(1)将该节点对应的线程置为null
node.thread = null;
//(2)跳过当前节点之前已经取消的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//获取在(2)操作之后,节点的下一个节点
Node predNext = pred.next;
//(3)将当前中断的线程对应节点状态设置为CANCELLED
node.waitStatus = Node.CANCELLED;
//(4)如果当前中断的节点是尾节点,那么则将尾节点重新指向
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
//(5)如果中断的节点的上一个节点的状态,为SINGAL或者即将为SINGAL,
//那么将该当前中断节点移除
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);//(6)将该节点移除,同时唤醒下一个节点
}
node.next = node; // help GC
}
}
//获取中断节点后的可用节点(Node.waitStatus<=0),然后将该节点对应的线程唤醒。
private void unparkSuccessor(Node node) {
//重置该节点为初始状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//获取中断节点的下一节点
Node s = node.next;
//判断下一节点的状态,如果为Node.CANCELED状态
if (s == null || s.waitStatus > 0) {
s = null;
//则通过尾节点向前遍历,获取最近的waitStatus<=0的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果该节点不会null,则唤醒该节点中的线程。
//这里要注意理论上可能头节点唤醒下一个节点的时候,下一个节点还没有通过park方法阻塞,而LockSupport方法在这种情况的表现是:
//如果先调用了unpark方法,那么之后调用park时将不会阻塞。因此在这种情况下也不会有什么问题。
if (s != null)
LockSupport.unpark(s.thread);
}
线程获取同步状态成功并执行相应逻辑后,需要释放同步状态,使得后继线程节点能够继续获取同步状态,通过调用AQS的relase(int arg)
方法,可以释放同步状态。
boolean release(int arg)
独占式释放同步状态,该方法会在释放同步状态后将第一个节点(对应刚刚释放同步状态的线程)的后继节点对应的线程唤醒。
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(int arg),也就是说同步状态的释放逻辑,是需要用户来自己定义的。
当tryRelease(int arg)
方法返回true后,如果当前头节点不为null且头节点waitStatus!=0,接着会调用unparkSuccessor(Node node)方法来唤醒下一节点(使其尝试获取同步状态)。
也就是说,当tryRelease操作成功后(也就是当前节点完全释放了锁),release操作才能检查是否需要唤醒下一个继任节点。这里的前提是AQS队列的头结点需要锁(waitStatus!=0),如果头结点需要锁,就开始检测下一个继任节点是否需要锁操作。
关于unparkSuccessor(Node node)方法,上文已经分析过了,这里就不再进行描述了。
共享式获取与独占式获取最主要的区别在于 同一时刻是否能有多个线程同时获取到同步状态。
我们举个例子,比如我们要定义一个类似于Semaphore的同步组件:支持n个线程可以同时获取同步状态,超过n时则阻塞
假如AQS没有给我们提供共享式的tryAcquireShared和tryReleaseShared方法,我们试图用独占式方法来实现这个组件,那么我们可能会这样重写tryAcquire和tryRelease(只贴出AQS的子类实现,其他代码略):
private static class SemaphoreSynchronizer extends AbstractQueuedSynchronizer {
public SemaphoreSynchronizer(int arg) {
setState(arg); //用state表示当前可用许可数
}
@Override
protected boolean tryAcquire(int arg) {
for (;;) {
int state = getState();
int newState = state - 1; //许可数-1
//如果已经没有许可可用,则返回false
if (newState < 0) {
return false;
}
//如果有许可可用而且CAS成功,则返回true,否则循环重新判断是否有许可可用
if (compareAndSetState(state, newState)) {
return true;
}
}
}
@Override
protected boolean tryRelease(int arg) {
for (;;) {
int current = getState();
int newCount = current + 1; //释放成功则许可数+1
//如果释放成功返回true,否则循环重新释放
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
protected Condition newCondition() {
return new ConditionObject();
}
}
这样实现的话仔细想就会发现有问题:
比如许可数设为3,当前正有t1、t2、t3这三个线程在运行,然后来了两个线程t4、t5被阻塞了,因为t1、t2、t3是并发运行,因此假设t1和t2同时释放许可,独占式释放同步状态代码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
这两个线程在并发条件下,Node h=head这句可能h同时指向t4,然后t4被唤醒2次,最终的结果就是t3和t4在执行,而t5在被阻塞,尽管有效许可数是3。
为了避免这个问题,需要在共享式同步状态的释放和获取处都做一些工作。
当线程获取同步状态成功并执行相应逻辑后,需要释放同步状态,使得后继线程节点能够继续获取同步状态,通过调用AQS的 releaseShared(int arg)
方法,可以释放同步状态。
主要的不同在于:遍历链表唤醒多个节点
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果首节点的状态是SIGNAL,则CAS修改SIGNAL为0,如果成功就唤醒后继节点,失败则重新获取首节点
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);//唤醒下一节点线程,上文分析过该方法,这里就不在讲了
}
//(2)表示该节点线程已经获取共享状态成功,则通过CAS操作将该线程节点状态设置为Node.PROPAGATE
//从上图中,我们可以得知在共享式的同步队列中,
else if (ws == 0 && //如果首节点状态是0,则将状态改为PROPAGATE(传播状态)
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 这个是外面一层for循环的终止条件,外面一层循环的意义在于如果首节点在以上操作中发生了变化,那么可能有其他节点已经唤醒了之前获取的首节点的后继节点,于是当前线程要获取新的首节点的后继节点。
if (h == head)
break;
}
}
acquireShared(int arg)
共享式的获取同步状态(该方法会调用子类重写的tryAcquireShared(int arg))
. 如果当前线程未获取到同步状态,将会进入同步队列的尾部等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
//(1)添加共享式节点在AQS中FIFO队列中
final Node node = addWaiter(Node.SHARED);
//锁资源获取失败标记位
boolean failed = true;
try {
boolean interrupted = false;
//(2)自旋获取同步状态
for (;;) {
final Node p = node.predecessor();//获取当前节点的前置节点
if (p == head) {
int r = tryAcquireShared(arg); //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
if (r >= 0) {
setHeadAndPropagate(node, r);//当获取同步状态成功后,设置head指针,将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
if (interrupted)
selfInterrupt();
failed = false;//表示锁资源成功获取,因此把failed置为false
return;
}
}
//(3)判断线程是否需要阻塞,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//(4)如果线程已经中断,则唤醒下一节点
if (failed)
cancelAcquire(node);
}
}
doAcquireShared(int arg)
整体来看,共享式获取的逻辑与独占式获取的逻辑几乎一样,还是以下几个步骤:
acquireQueued
自旋式循环获取同步状态,以达到目的"自旋式竞争锁或者阻塞当前线程和重置pre关系"
我们知道自旋式操作中有着需要唤醒线程后,继续竞争锁的功能,如果是独占式锁则仅当前线程节点竞争即可,但是共享式锁则要求当前节点也要及时唤醒后续节点
共享式与独占式获取同步状态的主要不同在于其设置head指针的方式不同,一个节点在获取了同步状态后,不仅把自己设置为头节点,而且如果当前同步状态>0||原head为null||原head的状态<0||当前head为null||当前状态<0,且下一个节点的类型为null(类型未知)||下一个节点类型为shared,则继续唤醒下一个节点。
注:节点状态<0意味着是SIGNAL或PROPAGATE。
下面我们就来看看共享式设置head指针的方法setHeadAndPropagate(Node node, int propagate)
// 第一个参数node是当前共享式获取同步状态的线程节点。
// 第二个参数是共享式获取同步状态线程节点的个数。 对应tryAcquireShared的返回值
private void setHeadAndPropagate(Node node, int propagate) {
//(1)设置head 指针,指向该节点
Node h = head; // Record old head for check below
setHead(node);
//(2)判断是否执行doReleaseShared();
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果当前节点的下一节点是共享式获取同步状态节点,则调用doReleaseShared()方法
if (s == null || s.isShared())
doReleaseShared();
}
}
关于怎么判断下一节点是否是否共享式线程节点,具体逻辑如下:
//在共享式访问中,当前节点为SHARED类型
final Node node = addWaiter(Node.SHARED);
//在调用addWaiter 内部会调用Node构造方法,其中会将nextWaiter设置为Node.SHARED。
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//SHARED为Node类静态类
final boolean isShared() {
return nextWaiter == SHARED;
}
其主要逻辑步骤分为以下两个步骤:
方法 | 描述 |
---|---|
void acquireInterruptibly(int arg) | 与 void acquire(int arg)基本逻辑相同,但是该方法响应中断。 如果当前没有获取到同步状态,那么就会进入等待队列,如果当前线程被中断(Thread().interrupt()),那么该方法将会抛出InterruptedException。并返回 |
void acquireSharedInterruptibly(int arg) | 在acquireShared(int arg)的基本逻辑相同,增加了响应中断。 |
独占式大体上相当于前面的acquireQueued,关键的区别在于检测到interrupted后的处理,acquireQueued简单的记录下中断曾经发生,然后继续去尝试获取锁,失败则休眠。 而doAcquireInterruptibly检测到中断则直接退出循环,抛出InterruptedException异常
/**
* 独占模式获取锁,如果在获取锁的过程中线程被中断,则直接抛出中断异常
*/
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
/**
* 大体上相当于前面的acquireQueued,关键的区别在于检测到interrupted后的处理,
* acquireQueued简单的记录下中断曾经发生,然后继续去尝试获取锁,失败则休眠。
* 而doAcquireInterruptibly检测到中断则直接退出循环,抛出InterruptedException异常
*
* @param arg 获得资源的参数
*/
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//在当前线程创建一个独占模式节点
final Node node = addWaiter(Node.EXCLUSIVE);
//锁资源获取失败标记位
boolean failed = true;
try {
for (;;) {//【1】再次获取同步状态
final Node p = node.predecessor();//获取当前节点的上一节点
//如果前置节点就是头结点,则尝试获取锁资源
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
if (p == head && tryAcquire(arg)) {
//设置head指向该节点,头结点就表示当前正占有锁资源的节点
setHead(node);
p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
failed = false; //表示锁资源成功获取,因此把failed置为false
return;
}
//检查和更新未能获取的节点的状态。如果线程阻塞则进入挂起逻辑
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();//【!!!!】抛出异常
}
} finally {
if (failed)
//【3】如果线程中断了,那么就将该线程从同步队列中移除,同时唤醒下一节点
cancelAcquire(node);
}
}
共享式 可中断式获取同步状态 也是同样的道理
/**
共享模式获取锁,如果在获取锁的过程中线程被中断,则直接抛出中断异常
*/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
/**
* 在可中断模式下获取资源
* @param arg the acquire argument
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//(1)添加共享式节点在AQS中FIFO队列中
final Node node = addWaiter(Node.SHARED);
//锁资源获取失败标记位
boolean failed = true;
try {
//(2)自旋获取同步状态
for (;;) {
final Node p = node.predecessor();//获取当前节点的前置节点
//如果前置节点就是头结点,则尝试获取锁资源,如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
if (p == head) {
int r = tryAcquireShared(arg); //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
if (r >= 0) {
setHeadAndPropagate(node, r);//当获取同步状态成功后,设置head指针,将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
failed = false; //表示锁资源成功获取,因此把failed置为false
return;
}
}
//(3)判断线程是否需要阻塞,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); //【!!!!!!】
}
} finally {
if (failed)
//最后会分析获取锁失败处理逻辑
cancelAcquire(node);
}
}
因为独占式与共享式超时获取同步状态,与其本身的非超时获取同步状态逻辑几乎一样。所以下面就以独占式超时获取同步状态的相应逻辑进行讲解。
tryAcquireNanos(int arg, long nanosTimeout)
在acquireInterruptibly(int arg)
的基础上,增加了超时限制,如果在超时时间内获取到同步状态返回true,否则返回false
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 {
if (nanosTimeout <= 0L)
return false;
//(1)计算超时等待的结束时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//(2)如果获取同步状态成功,直接返回
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//如果获取同步状态失败,计算的剩下的时间
nanosTimeout = deadline - System.nanoTime();
//(3)如果超时直接退出
if (nanosTimeout <= 0L)
return false;
//(4)如果没有超时,且nanosTimeout大于spinForTimeoutThreshold(1000纳秒)时,
//则让线程等待nanosTimeout (剩下的时间,单位:纳秒。)
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//(5)如果当前线程被中断,直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
整个方法为以下几个步骤:
方法 | 描述 |
---|---|
boolean hasQueuedThreads() | 此方法返回 Boolean 值表明是否有线程在等待获取此锁,根据head、tail |
boolean hasContended() | 判断是否有节点,无论是阻塞节点还是已获取到的节点 |
Thread getFirstQueuedThread() | 返回队列里第一个没有获取到锁的线程,如果head等于tail说明队列里没有线程在等待,直接返回null;否则,调用fullGetFirstQueuedThread。 |
boolean isQueued(Thread thread) | 判断线程是否在队列里(包含头节点了) |
boolean hasQueuedPredecessors() | 通过判断"当前线程"是不是在CLH队列的队首,来返回AQS中是不是有比“当前线程”等待更久的线程 |
int getQueueLength() | 此方法返回等待获取此锁的线程数量 |
Collection getQueuedThreads() | 返回一个 collection,它包含可能正等待获取此锁的线程 |
Collection getExclusiveQueuedThreads() | 返回包含可能正以独占模式等待获取的线程 collection |
Collection getSharedQueuedThreads() | 返回包含可能正以共享模式等待获取的线程 collection |
boolean owns(ConditionObject condition) | |
boolean hasWaiters(ConditionObject condition) | |
int getWaitQueueLength(ConditionObject condition) | 返回等待与此锁定相关的给定条件condition的线程估计数 |
Collection getWaitingThreads(ConditionObject condition) | 返回一个 collection,该 collection 包含那些正在与此同步器关联的给定条件上等待的线程。 |
/**
查询是否有线程正在等待获取指定的对象监视器
*/
public final boolean hasQueuedThreads() {
return this.head != this.tail;
}
/**
查询队列是否被多个acquire请求竞争过(导致某个线程阻塞过)。为什么head 不为null就能证明?有竞争就会入队列此时head不为null,但是任务执行完了呢?
通过上面的代码知道,head是由队列里刚获得到锁的线程设置的
(把自己设置成head),即使任务执行完也不会修改head,只能由下个入队的线程设置,这样head就永远不会为空了。
*/
public final boolean hasContended() {
return this.head != null;
}
/**
返回队列里第一个没有获取到锁的线程,如果head等于tail说明队列里没有线程在等待,直接返回null;否则,调用fullGetFirstQueuedThread。
*/
public final Thread getFirstQueuedThread() {
return this.head == this.tail?null:this.fullGetFirstQueuedThread();
}
/**
判断线程是否在队列里(包含头节点了),跟getFirstQueuedThread不一样,没有先从head开始找,直接从tail开始反向搜索,很直接。
因为getFirstQueuedThread要找的是第一个,从head开始找效率比较高,从tail开始反向遍历是因为没有其他更好的方法了。
isQueued不一样,它是找一个节点,反正都要遍历一遍,从head或tail都一样,时间复杂度都是O(n)。
*/
public final boolean isQueued(Thread thread) {
if (thread == null)
throw new NullPointerException();
for (Node p = tail; p != null; p = p.prev)
if (p.thread == thread)
return true;
return false;
}
/**
通过判断"当前线程"是不是在CLH队列的队首,来返回AQS中是不是有比“当前线程”等待更久的线程
* @since 1.7
*/
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
/**
* 队列长度,不包括已经取消的和头节点,因为它俩的thread域都为null。
*/
public final int getQueueLength() {
int n = 0;
for (Node p = tail; p != null; p = p.prev) {
if (p.thread != null)
++n;
}
return n;
}
/**
返回一个 collection,它包含可能正等待获取此锁的线程
*/
public final Collection getQueuedThreads() {
ArrayList list = new ArrayList();
for (Node p = tail; p != null; p = p.prev) {
Thread t = p.thread;
if (t != null)
list.add(t);
}
return list;
}
/**
* 返回包含可能正以独占模式等待获取的线程 collection
*/
public final Collection getExclusiveQueuedThreads() {
ArrayList list = new ArrayList();
for (Node p = tail; p != null; p = p.prev) {
if (!p.isShared()) {
Thread t = p.thread;
if (t != null)
list.add(t);
}
}
return list;
}
/**
* 返回包含可能正以共享模式等待获取的线程 collection
*/
public final Collection getSharedQueuedThreads() {
ArrayList list = new ArrayList();
for (Node p = tail; p != null; p = p.prev) {
if (p.isShared()) {
Thread t = p.thread;
if (t != null)
list.add(t);
}
}
return list;
}
public final boolean owns(ConditionObject condition) {
return condition.isOwnedBy(this);
}
public final boolean hasWaiters(ConditionObject condition) {
if (!owns(condition))
throw new IllegalArgumentException("Not owner");
return condition.hasWaiters();
}
/**
* 返回等待与此锁定相关的给定条件condition的线程估计数
*/
public final int getWaitQueueLength(ConditionObject condition) {
if (!owns(condition))
throw new IllegalArgumentException("Not owner");
return condition.getWaitQueueLength();
}
/**
* 返回一个 collection,该 collection 包含那些正在与此同步器关联的给定条件上等待的线程。
*/
public final Collection getWaitingThreads(ConditionObject condition) {
if (!owns(condition))
throw new IllegalArgumentException("Not owner");
return condition.getWaitingThreads();
}
}
更多源码请看: AQS源码