思考题
sync队列和condition队列的作用分别是什么?怎么协调的?
sync队列中的节点的状态有哪些?都是自旋还是有阻塞的?
是怎么争抢锁的?
sync队列是怎么保证FIFO的?
signal方法和release方法的区别?
一、简介
1、提供了一个基于FIFO队列,它是CLH队列的变种,CLH队列通常用于自旋锁 ,可以用于构建锁或者其他相关同步装置的基础框架。该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础。使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态。然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作:
getState() 、setState(int) 、compareAndSetState(int, int)
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
同步器中有两个状态量:state和waitState
state变量是针对整个同步器的,语义为锁定与否,默认初始值为0
waitState是AQS同步器的内部类Node类中的变量,代表的是节点状态,即节点代表的线程所处的状态。
2、该同步器即可以作为排他模式也可以作为共享模式,当它被定义为一个排他模式时,其他线程对其的获取就被阻止,而共享模式对于多个线程获取都可以成功。
3、内部有两个队列:sync同步队列和condition条件队列。这两个队列都是用链表实现的。不同的是,sync队列使用双向链表维护FIFO的队列,而condition条件队列是单向链表维护的FIFO队列。
4、AbstractQueuedSynchronizer类有两个内部类,分别为Node类与ConditionObject类
二、Condition接口介绍及原理
执行流程
1、Condition提供了await()方法将当前线程阻塞,并提供signal()方法支持另外一个线程将已经阻塞的线程唤醒。 2、Condition需要结合Lock使用
3、线程调用await()方法前必须获取锁,调用await()方法时,将线程构造成节点加入等待队列,同时释放锁,并挂起当前线程
4、其他线程调用signal()方法前也必须获取锁,当执行signal()方法时将等待队列的节点移入到同步队列,当线程退出临界区释放锁的时候(ReentrantLock.unlock),唤醒同步队列的首个节点
关于Condition接口
在并发编程中,每个Java对象都存在一组监视器方法,如wait()、notify()以及notifyAll()方法,通过这些方法,我们可以实现线程间通信与协作(也称为等待唤醒机制),而且这些方法必须配合着synchronized关键字使用,与synchronized(+wait()、notify())的等待唤醒机制相比Condition(+Lock+await()、signal())具有更多的灵活性以及精确性,这是因为notify()在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制(Lock,Condition ,await,signal),也就带来了更多灵活性了,我们可以简单理解为以下两点
- 通过Condition能够精细的控制多线程的休眠与唤醒。
- 对于一个锁,我们可以为多个线程间建立不同的Condition。
- Condition接口是对Object方法的wait、notify等线程通信方法的优化,是等待唤醒机制的升级
- 线程之间的通信(等待-唤醒)是通过signal进行信号的传输
- Condition接口与Object类的方法对应关系:await—wait 、signal—notify 、 signalAll ----nofifyAll
Condition接口的源码
public interface Condition {
/**
* 使当前线程进入等待状态直到被通知(signal)或中断
* 当其他线程调用singal()或singalAll()方法时,该线程将被唤醒
* 当其他线程调用interrupt()方法中断当前线程
* await()相当于synchronized等待唤醒机制中的wait()方法
*/
void await() throws InterruptedException;
//当前线程进入等待状态,直到被唤醒,该方法不响应中断要求
void awaitUninterruptibly();
//调用该方法,当前线程进入等待状态,直到被唤醒或被中断或超时
//其中nanosTimeout指的等待超时时间,单位纳秒
long awaitNanos(long nanosTimeout) throws InterruptedException;
//同awaitNanos,但可以指明时间单位
boolean await(long time, TimeUnit unit) throws InterruptedException;
//调用该方法当前线程进入等待状态,直到被唤醒、中断或到达某个时
//间期限(deadline),如果没到指定时间就被唤醒,返回true,其他情况返回false
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须
//获取与Condition相关联的锁,功能与notify()相同
void signal();
//唤醒所有等待在Condition上的线程,该线程从等待方法返回前必须
//获取与Condition相关联的锁,功能与notifyAll()相同
void signalAll();
}
Condition的实现原理
Condition的具体实现类是AQS的内部类ConditionObject,前面我们分析过AQS中存在两种队列,一种是同步队列,一种是等待队列,而等待队列就相对于Condition而言的。注意在使用Condition前必须获得锁,同时在Condition的等待队列上的结点与前面同步队列的结点是同一个类即Node,其结点的waitStatus的值为CONDITION。在实现类ConditionObject中有两个结点分别是firstWaiter和lastWaiter,firstWaiter代表等待队列第一个等待结点,lastWaiter代表等待队列最后一个等待结点,如下
public class ConditionObject implements Condition, java.io.Serializable {
//等待队列第一个等待结点
private transient Node firstWaiter;
//等待队列最后一个等待结点
private transient Node lastWaiter;
//省略其他代码.......
}
每个Condition都对应着一个等待队列!!,也就是说如果一个锁上创建了多个Condition对象,那么也就存在多个等待队列。等待队列是一个FIFO的队列,在队列中每一个节点都包含了一个线程的引用,而该线程就是Condition对象上等待的线程。当一个线程调用了await()相关的方法,那么该线程将会释放锁,并构建一个Node节点封装当前线程的相关信息加入到等待队列中进行等待,直到被唤醒、中断、超时才从队列中移出。
个人理解:在同步器中的Condition接口实例,在我们这里讲的ConditionObect对象,其实,是管理同步器对应线程之间的通信的一个对象。这个对象管理着一个Condition等待队列,这个队列存放着等待(等待被唤醒)的线程.
同时注意:这个等待队列虽然和sync同步队列几乎一样,但是等待队列是一个单向链表的一个队列,而sync队列是双向链表。
Node节点的数据结构,在等待队列中使用的变量与同步队列是不同的,Condtion中等待队列的结点只有直接指向的后继结点并没有指明前驱结点,而且使用的变量是nextWaiter而不是next,这点我们在前面分析结点Node的数据结构时讲过。firstWaiter指向等待队列的头结点,lastWaiter指向等待队列的尾结点,等待队列中结点的状态只有两种即CANCELLED和CONDITION,前者表示线程已结束需要从等待队列中移除,后者表示条件结点等待被唤醒。再次强调每个Codition对象对于一个等待队列,也就是说AQS中只能存在一个同步队列,但可拥有多个等待队列。
ConditionObject类的源码
public final void await() throws InterruptedException {
//判断线程是否被中断
if (Thread.interrupted())
throw new InterruptedException();
//创建新结点加入等待队列并返回
Node node = addConditionWaiter();
//释放当前线程锁即释放同步状态
int savedState = fullyRelease(node);
int interruptMode = 0;
//判断结点是否同步队列(SyncQueue)中,即是否被唤醒
while (!isOnSyncQueue(node)) {
//挂起线程
LockSupport.park(this);
//判断是否被中断唤醒,如果是退出循环。
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//被唤醒后执行自旋操作争取获得锁,同时判断线程是否被中断
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// clean up if cancelled
if (node.nextWaiter != null)
//清理等待队列中不为CONDITION状态的结点
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
执行addConditionWaiter()添加到等待队列。
private Node addConditionWaiter() {
Node t = lastWaiter;
// 判断是否为结束状态的结点并移除
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//创建新结点状态为CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//加入等待队列
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
await()方法主要做了3件事,一是调用addConditionWaiter()方法将当前线程封装成node结点加入等待队列,二是调用fullyRelease(node)方法释放同步状态并唤醒后继结点的线程。三是调用isOnSyncQueue(node)方法判断结点是否在同步队列中(即await后一直卡在这里,while循环判断是否已经被其他线程signal唤醒),如果同步队列中没有该结点就直接挂起该线程,需要明白的是如果线程被唤醒后就调用acquireQueued(node, savedState)执行自旋操作争取锁,即当前线程结点从等待队列转移到同步队列并开始努力获取锁。
接着看看唤醒操作singal()方法 ,可以看到signal联系的是条件队列中的第一个节点,将其唤醒,进入同步队列
public final void signal() {
//判断是否持有独占锁,如果不是抛出异常,说明了signal方法需要在独占锁的条件下使用
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter; //这里联系的是条件队列的第一个节点
//唤醒条件队列第一个结点的线程,满足FIFO
if (first != null)
doSignal(first);
}
这里signal()方法做了两件事,一是判断当前线程是否持有独占锁,没有就抛出异常,从这点也可以看出只有独占模式先采用等待队列,而共享模式下是没有等待队列的,也就没法使用Condition。二是唤醒等待队列的第一个结点,即执行doSignal(first)
private void doSignal(Node first) {
do {
//移除条件等待队列中的第一个结点,
//如果后继结点为null,那么说没有其他结点将尾结点也设置为null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
//如果被通知节点没有进入到同步队列并且条件等待队列还有不为空的节点,则继续循环通知后续结点
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
//transferForSignal方法
final boolean transferForSignal(Node node) {
//尝试设置唤醒结点的waitStatus为0,即初始化状态
//如果设置失败,说明当期结点node的waitStatus已不为
//CONDITION状态,那么只能是结束状态了,因此返回false
//返回doSignal()方法中继续唤醒其他结点的线程,注意这里并
//不涉及并发问题,所以CAS操作失败只可能是预期值不为CONDITION,
//而不是多线程设置导致预期值变化,毕竟操作该方法的线程是持有锁的。
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//加入同步队列并返回前驱结点p
Node p = enq(node); //可知enq不仅是初始化同步队列,也可以添加节点
int ws = p.waitStatus;
//判断前驱结点是否为结束结点(CANCELLED=1)或者在设置
//前驱节点状态为Node.SIGNAL状态失败时,唤醒被通知节点代表的线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//唤醒node结点的线程
LockSupport.unpark(node.thread);
return true;
}
注释说得很明白了,这里我们简单整体说明一下,doSignal(first)方法中做了两件事,从条件等待队列移除被唤醒的节点,然后重新维护条件等待队列的firstWaiter和lastWaiter的指向。二是将从等待队列移除的结点加入同步队列(在transferForSignal()方法中完成的),如果进入到同步队列失败并且条件等待队列还有不为空的节点,则继续循环唤醒后续其他结点的线程。到此整个signal()的唤醒过程就很清晰了,即signal()被调用后,先判断当前线程是否持有独占锁,如果有,那么唤醒当前Condition对象中等待队列的第一个结点的线程,并从等待队列中移除该结点,移动到同步队列中,如果加入同步队列失败,那么继续循环唤醒等待队列中的其他结点的线程,如果成功加入同步队列,那么如果其前驱结点是否已结束或者设置前驱节点状态为Node.SIGNAL状态失败,则通过LockSupport.unpark()唤醒被通知节点代表的线程,到此signal()任务完成,注意被唤醒后的线程,将从前面的await()方法中的while循环中退出,因为此时该线程的结点已在同步队列中,那么while (!isOnSyncQueue(node))将不在符合循环条件,进而调用AQS的acquireQueued()方法加入获取同步状态的竞争中,这就是等待唤醒机制的整个流程实现原理
三、sync同步队列和condition等待队列
简述
AbstractQueuedSynchronizer类底层的数据结构是使用链表,是队列的一种实现,故也可看成是队列,其中Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度。而Condition queue即等待队列不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue。Condition的具体实现类是AQS的内部类ConditionObject
AQS自己维护的队列是当前等待资源的队列 ,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
而Condition自己也维护了一个等待队列(等待signal信号的队列),两个队列的作用是不同,事实上,每个线程也仅仅会同时存在以上两个队列中的一个
Node类
//AQS中的Node属性
static final class Node {
...
volatile int waitStatus;//等待状态
volatile Node prev;//前驱节点
volatile Node next;//后驱节点
volatile Thread thread;//获取同步状态的线程,当前执行线程
Node nextWaiter;//等待队列中的后继节点
...
}
同步队列和等待队列使用的是同一个Node类型AbstractQueuedSynchronizer.Node
同步队列和等待队列的协同机制
1、同步队列中包含线程A(节点A)和线程B(节点B),线程调用reentrantLock.lock()时,线程被加入到AQS同步队列中
2、线程A(节点A)调用condition.await()方法时,线程A(节点A)从AQS同步队列中被移除,对应操作是锁的释放; 线程A(节点A)接着被加入到Condition等待队列,因为线程需要signal信号。
3、线程B(节点B)由于线程A(节点A)释放锁被唤醒,判断成为同步队列头结点且同步状态为0可以获取锁;线程B(节点B)获取锁。
4、线程B(节点B)调用signal()方法,Condition等待队列中有一个节点A,把它取出来(A)加入到AQS同步队列中。这时候线程A(节点A)并没有被唤醒。
5、线程B(节点B)signal方法执行完毕,并调用reentrantLock.unLock()方法释放锁。线程A(节点A)成为AQS首节点并且同步状态可获取,线程A(节点A)被唤醒,继续执行。
6、AQS从头到尾顺序唤醒线程,直到等待队列中的线程被执行完毕结束。
7、只有发送singal信号的线程调用reentrantLock.unLock()后,因为它已经被加入到AQS同步队列并且成为同步队列头结点,所以线程才会被唤醒。
同步器和锁的概念
**同步器是实现锁的关键,**利用同步器将锁的语义实现,然后在锁的实现中聚合同步器。可以这样理解:锁的API是面向使用者的,它定义了与锁交互的公共行为,而每个锁需要完成特定的操作也是透过这些行为来完成的(比如:可以允许两个线程进行加锁,排除两个以上的线程),但是实现是依托给同步器来完成;同步器面向的是线程访问和资源控制,它定义了线程对资源是否能够获取以及线程的排队等操作。锁和同步器很好的隔离了二者所需要关注的领域,严格意义上讲,同步器可以适用于除了锁以外的其他同步设施上(包括锁)。
同步器的开始提到了其实现依赖于一个FIFO队列,那么队列中的元素Node就是保存着线程引用和线程状态的容器,每个线程对同步器的访问,都可以看做是队列中的一个节点。Node的主要包含以下成员变量:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1; //表示当前的线程被取消
static final int SIGNAL = -1; //表示当前节点的后继节点包含的线程需要运行,也就是unpark
static final int CONDITION = -2; //表示当前节点在等待condition,也就是在condition队列中
static final int PROPAGATE = -3; //表示当前场景下后续的acquireShared能够得以执行
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter; //存储condition队列中的后继节点。
}
waitStatus的值默认为0,表示当前节点在sync队列中,等待着获取锁 。
这个节点是构成sync队列和condition队列的基础
四、排他锁的示例
class Mutex implements Lock, java.io.Serializable {
// 内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0的时候获取锁
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为0
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// Provides a Condition
Condition newCondition() { return new ConditionObject(); }
// Deserializes properly
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
// The sync object does all the hard work. We just forward to it.
private final Sync sync = new Sync();
public void lock() { sync.acquire(1); }
public boolean tryLock() { return sync.tryAcquire(1); }
public void unlock() { sync.release(1); }
public Condition newCondition() { return sync.newCondition(); }
public boolean isLocked() { return sync.isHeldExclusively(); }
public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}}
五、AQS源码
Node节点中waitStatus 的属性值代表的状态
-
CANCELLED,值为1,由于超时或中断,该节点被取消。 节点进入该状态将不再变化。特别是具有取消节点的线程永远不会再次阻塞
-
SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
-
CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
-
PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;
-
值为0,表示当前节点在sync队列中,等待着获取锁。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** * The synchronization state. * state变量表示锁的状态 * 0 表示未锁定 * 大于0表示已锁定 * 需要注意的是,这个值可以用来实现锁的【可重入性】,例如 state=3 就表示锁被同一个线程获取了3次,想要完全解锁,必须要对应的解锁3次 * 同时这个变量还是用volatile关键字修饰的,保证可见性 */ private volatile int state; /** * 同步队列的头节点,只能通过setHead方法修改 * 如果head存在,能保证waitStatus状态不为CANCELLED */ private transient volatile Node head; /** * 同步队列的尾结点 */ private transient volatile Node tail;
}
该方法以排他的方式获取锁,对中断不敏感,完成synchronized语义。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述逻辑主要包括: 1. 尝试获取(调用tryAcquire更改状态,需要保证原子性); 在tryAcquire方法中使用了同步器提供的对state操作的方法,利用compareAndSet保证只有一个线程能够对状态进行成功修改,而没有成功修改的线程将进入sync队列排队。 2. 如果获取不到,将当前线程构造成节点Node并加入sync队列; 进入队列的每个线程都是一个节点Node,从而形成了一个双向队列,类似CLH队列,这样做的目的是线程间的通信会被限制在较小规模(也就是两个节点左右)。 3. 再次尝试获取,如果没有获取到那么将当前线程从线程调度器上摘下,进入等待状态。 使用LockSupport将当前线程unpark,关于LockSupport后续会详细介绍。
添加等待者到同步队列
private Node addWaiter(Node mode) {
// 新生成一个结点,默认为独占模式
Node node = new Node(Thread.currentThread(), mode);
// 快速尝试在尾部添加
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { // 使用CAS算法入队并设置为尾节点
pred.next = node;
return node;
}
}
enq(node); //尝试快速添加失败,表示队列未初始化,调用此方法初始化队列
return node;
}
上述逻辑主要包括: 1. 使用当前线程构造Node; 对于一个节点需要做的是将当节点前驱节点指向尾节点(current.prev = tail),尾节点指向它(tail = current),原有的尾节点的后继节点指向它(t.next = current)而这些操作要求是原子的。上面的操作是利用尾节点的设置来保证的,也就是compareAndSetTail来完成的。 2. 先行尝试在队尾添加; 如果尾节点已经有了,然后做如下操作: (1)分配引用T指向尾节点; (2)将节点的前驱节点更新为尾节点(current.prev = tail); (3)如果尾节点是T,那么将当尾节点设置为该节点(tail = current,原子更新); (4)T的后继节点指向当前节点(T.next = current)。 注意第3点是要求原子的。 这样可以以最短路径O(1)的效果来完成线程入队,是最大化减少开销的一种方式。 3. 如果队尾添加失败或者是第一个入队的节点。 如果是第1个节点,也就是sync队列没有初始化,那么会进入到enq这个方法,进入的线程可能有多个,或者说在addWaiter中没有成功入队的线程都将进入enq这个方法。 可以看到enq的逻辑是确保进入的Node都会有机会顺序的添加到sync队列中,而加入的步骤如下: (1)如果尾节点为空,那么原子化的分配一个头节点,并将尾节点指向头节点,这一步是初始化; (2)然后是重复在addWaiter中做的工作,但是在一个while(true)的循环中,直到当前节点入队为止。 进入sync队列之后,接下来就是要进行锁的获取,或者说是访问控制了,只有一个线程能够在同一时刻继续的运行,而其他的进入等待状态。而每个线程都是一个独立的个体,它们自省的观察,当条件满足的时候(自己的前驱是头结点并且原子性的获取了状态),那么这个线程能够继续运行。
用于初始化队列,也可以添加操作
private Node enq(final Node node) {
for (;;) { // 无限循环,确保结点能够成功入队列
Node t = tail;
if (t == null) { // 表示当前同步队列未初始化,需要进行初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { //原子操作
t.next = node;
return t;
}
}
}
}
说明:enq函数会使用无限循环来确保节点的成功插入。
sync队列中的结点在独占且忽略中断的模式下获取(资源)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 无限循环
final Node p = node.predecessor(); // 获取node节点的前驱结点
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); 自旋异常退出,取消正在进行锁争抢
}
}
上述逻辑主要包括: 1. 获取当前节点的前驱节点; 需要获取当前节点的前驱节点,而头结点所对应的含义是当前站有锁且正在运行。 2. 当前驱节点是头结点并且能够获取状态,代表该当前节点占有锁; 如果满足上述条件,那么代表能够占有锁,根据节点对锁占有的含义,设置头结点为当前节点。 3. 否则进入等待状态。 如果没有轮到当前节点运行,那么将当前线程从线程调度器上摘下,也就是进入等待状态。 这里针对acquire做一下总结: 1. 状态的维护; 需要在锁定时,需要维护一个状态(int类型),而对状态的操作是原子和非阻塞的,通过同步器提供的对状态访问的方法对状态进行操纵,并且利用compareAndSet来确保原子性的修改。 2. 状态的获取; 一旦成功的修改了状态,当前线程或者说节点,就被设置为头节点。 3. sync队列的维护。 在获取资源未果的过程中条件不符合的情况下(不该自己,前驱节点不是头节点或者没有获取到资源)进入睡眠状态,停止线程调度器对当前节点线程的调度。 这时引入的一个释放的问题,也就是说使睡眠中的Node或者说线程获得通知的关键,就是前驱节点的通知,而这一个过程就是释放,释放会通知它的后继节点从睡眠中返回准备运行。
从上述代码中看到,只有前驱节点是头结点才能够尝试获取同步状态,这时为什么呢?原因有两个:
- 头结点是成功获取到同步状态的节点,而头结点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点被唤醒后需要检查自己的前驱节点是否为头结点;
- 维护同步队列的FIFO原则
用于判断当前线程是否需要被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱节点的等待状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* SIGNAL表示后继节点处于等待状态,如果当前节点释放了锁或者被取消,会通知后继节点去运行
* 所以作为后继节点,node直接返回true,表示需要被阻塞
*/
return true;
if (ws > 0) {
/*
* 前驱节点被取消了,需要从队列中移除,并且循环找到下一个不是取消状态的节点
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 通过CAS将前驱节点的status设置成SIGNAL
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
用于阻塞线程并且检测线程是否被中断
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
LockSupport.park(this);
// 检测当前线程是否被中断(该方法会清除中断标识位)
return Thread.interrupted();
}
取消正在尝试获取锁(acquire)的线程
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// 保存node的前驱结点
Node pred = node.prev;
while (pred.waitStatus > 0)// 找到node前驱结点中第一个状态小于0的结点,即不为CANCELLED状态的结点
node.prev = pred = pred.prev;
Node predNext = pred.next; // 获取pred结点的下一个结点
// 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
}
}
该函数完成的功能就是取消当前线程对资源的获取,即设置该结点的状态为CANCELLED
以独占模式释放对象
public final boolean release(int arg) {
if (tryRelease(arg)) { // 释放成功
Node h = head; // 保存头结点
if (h != null && h.waitStatus != 0) //头结点不为空并且头结点状态不为0
unparkSuccessor(h); //唤醒头结点的后继结点
return true;
}
return false;
}
上述逻辑主要包括: 1. 尝试释放状态; tryRelease能够保证原子化的将状态设置回去,当然需要使用compareAndSet来保证。如果释放状态成功过之后,将会进入后继节点的唤醒过程。 2. 唤醒当前节点的后继节点所包含的线程。 通过LockSupport的unpark方法将休眠中的线程唤醒,让其继续acquire状态。
个人小结:前驱节点,也就是获取到了锁的线程,同时代码逻辑执行完毕后,需要通过release方法来进行线程之间的通信,通知后继节点:我执行完了,锁给你们让出来了 ,你们醒醒,该去争夺锁资源了。
private void unparkSuccessor(Node node) {
//将状态设置为同步状态
int ws = node.waitStatus;
if (ws < 0) // 状态值小于0,为SIGNAL -1 或 CONDITION -2 或 PROPAGATE -3
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);
}
说明:该函数的作用就是为了释放node节点的后继结点。
个人小结:前驱节点执行完毕,调用release方法,release方法中又调用unparkSuccessor方法去唤醒后继节点,找到合适的节点后,调用LockSupport类的unpark方法去唤醒该节点。
但是,虽然获取后继节点首先是调用Node s = node.next,也就是同步队列中的首节点,这符合同步队列FIFO的要求,但是,下面代码中,如果首节点不符合要求,则从尾节点开始从后往前遍历,找到合适的节点,这里我有点不明白,这不就不符合FIFO的要求了吗,向大家指教。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
六、对思考题的解释
1、sync队列和condition队列的作用分别是什么?怎么协调的?
作用:sync队列是同步器主要执行的队列,是必须的。而condition队列不是必须的。
sync队列可以理解为是获取锁资源的队列,等待被调度器调度的队列。而condition队列是被阻塞(挂起、休眠)的队列,可以理解为是准备获取signal信号的队列。
sync队列管理的是线程调度、锁的获取释放。而等待队列管理的是线程阻塞唤醒、线程通信
协调:
一句话,通过signal信号。sync队列中的节点线程调用了signal方法时,表示后继节点可以开始获取同步状态(锁)了,这时,将会从等待队列中取得一个节点(一般是首节点)放到同步队列的尾部自旋,争夺锁
而sync队列中的节点如果获取锁失败(某种原因被阻塞),需要被打入condition队列中去。或者sync队列中的节点调用了await方法,也会释放掉锁,同时阻塞,被打入condition队列。
2、sync队列中的节点的状态有哪些?都是自旋还是有阻塞的?
sync队列中节点状态是用Node节点中的waitNode 属性表示,有四种是属于sync队列节点的:
CANCELLED、SIGNAL、PROPAGATE和默认的0
condition队列中的节点状态只有两种:CANCELLED和默认的CONDITION
对于CANCELLED来说,一般是由于超时或中断,该节点被取消。 节点进入该状态将不再变化。特别是具有取消节点的线程永远不会再次阻塞
sync队列的节点,当前驱节点,也就是获取到了锁正在执行的线程,将要释放锁时,调用signal方法来通知后继节点,换句话说,就是唤醒后继节点,这个时候,同步队列中的节点自旋式的争夺锁资源,但是,当某些条件下,如果不满足acquire(获取锁的条件),将会被阻塞,也就是放弃CPU的执行,换句话说, 就是不在自旋,而是进入等待状态,等待被唤醒。
同时,往sync队列添加新的等待节点,这个节点也会自旋式的争夺锁。
3、是怎么争抢锁的?
是对于sync队列的节点来说的,sync队列中的节点默认都是自旋式的争夺锁(同步状态),只有在某些条件下会被阻塞。
4、sync队列是怎么保证FIFO的?
实现细节在acquireQueued 这个方法。从出队和入队讲:
出队:acquireQueued 这个方法是sync队列中的结点在独占且忽略中断的模式下获取资源(锁),在这个方法中必须只有前驱节点是头结点才能够尝试获取同步状态 ,代码如下
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
入队:在addWaiter和enq方法中
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { // 使用CAS算法入队并设置为尾节点
pred.next = node;
return node;
}
一般线程获取不到锁,会调用addWaiter方法形成一个节点放到sync队列的尾部,而且是原子性的。如果是从condition队列中的节点搬到sync队列中,调用的是enq方法,也是创建一个节点放到sync队列的尾部,原子性的。
5、signal方法和release方法的区别?
signal方法用于线程通信,release方法是释放锁(同步状态)。
前驱节点,获取到锁,且正在执行,调用signal方法,通知后继节点争夺锁资源,在signal方法执行完之前,会调用release方法释放锁,同时,需要等待队列中的节点获取到锁后,signal方法才能结束。
七、总结
对于AbstractQueuedSynchronizer的分析,最核心的就是sync queue的分析。
1、每一个结点都是由前一个结点唤醒
2、当结点发现前驱结点是head并且尝试获取成功,则会轮到该线程运行。
3、condition queue中的结点向sync queue中转移是通过signal操作完成的。
4、当结点的状态为SIGNAL时,表示后面的结点需要运行。