AQS,全称是AbstractQueuedSynchronizer,抽象队列式同步器。
AQS是一个构建锁和同步器的框架,使用AQS快速构造出同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
AQS核心思想是如果请求资源空闲,那么就将将当前线程设置成有效线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要线程阻塞和等待的机制。这个机制由CLH队列锁实现,即将暂时获取不到的锁的线程加入到队列中。
原理
AQS是一个抽象类,当我们继承AQS实现自己的同步器是,AQS不关注怎么获取锁/释放锁(如何获取和释放锁,由具体子类实现),至于具体线程等待队列的维护(如获取锁失败入队、唤醒出队、以及线程在队列中行为的管理等),AQS在其顶层已经帮我们实现好了,AQS的这种设计使用的正是模板方法模式。
原理图
CLH
同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO(先入先出)队列构成等待队列。
CLH 是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
饥饿
一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态称之“饥饿”。
解决饥饿的方案被称之为“公平性”–即所有线程均能公平地获得运行机会。
AQS Class
CLH队列由Node对象组成,Node是AQS中的内部类。
state
AQS使用一个int成员变量来表示同步状态,通过内置的CLH队列来完成资源线程的排队工作。
AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
状态信息通过getState,setState,compareAndSetState进行操作
state(同步状态)
private volatile int state;
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
compareAndSetState通常用于在获取到锁之前,尝试加锁时,对state进行修改,这种场景下,由于当前线程不是锁持有者,所以对state的修改是线程不安全的,也就是说可能存在多个线程都尝试修改state,所以需要保证对state修改的原子性操作,所以使用了unsafe类的本地CAS方法;
setState方法通常用于当前正持有锁的线程对state变量进行修改,不存在竞争,是线程安全的,所以此处没必要用CAS保证原子性,修改的性能更重要。
Node
Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,
- AQS持有head指针和tail指针,头结点是抢占锁成功而持有锁的线程对应的结点,若有线程抢锁失败,AQS会创建新结点并用CAS操作使其成为新的尾结点
- AQS把对某线程的一些控制信息放到了其前驱中维护,当某结点的前驱释放锁或被取消时会唤醒其后继,而其后继会在获取锁成功后将自己设为新的头结点
- 可以看到,AQS对这个维护等待线程队列的操作都是非阻塞的,也是线程安全的。
Node节点状态
waitStatus; //结点的等待状态,CLH队列中初始默认为0,Condition队列中初始默认为-2
CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化,结点状态以后不再变直到GC回收它
SIGNAL(-1):表示后续节点中的线程通过park被阻塞了,当前节点在释放或取消时要通过unpark解除他的阻塞。而后继结点入队时,会将前继结点的状态更新为SIGNAL。
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
0:新结点入队时的默认状态。
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
Node prev:前驱节点,当节点加入同步队列的时候被设置(尾部添加)
Node next:后继节点
Node nextWaiter:等待节点的后继节点。如果当前节点是共享的,那么这个字段是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用一个字段。(注:比如说当前节点A是共享的,那么它的这个字段是shared,也就是说在这个等待队列中,A节点的后继节点也是shared。如果A节点不是共享的,那么它的nextWaiter就不是一个SHARED常量,即是独占的。)
- 通过nexwaiter 判断是不是共享锁
- AQS的阻塞队列是以双向的链表的形式保存的,是通过prev和next建立起关系的,但是AQS中的条件队列是以单向链表的形式保存的,是通过nextWaiter建立起关系的
AQS 定义两种资源共享方式
Exclusive(独占)
只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
核心方法 acquire--release
acquire(int arg)
入口
独占模式下线程获取资源的顶层入口,如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。
public final void acquire(int arg) { //独占模式获取锁的模板方法
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- tryAcquire()尝试直接去获取资源,如果成功则直接返回,具体实现有由实现AQS的实现类实现。 这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待
- addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- selfInterrupt() 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
addWaiter(Node)(加入队列)
/**
* 根据当前线程的获取锁模式创建一个结点并加入队列中
* @param mode Node.EXCLUSIVE表示独占模式, Node.SHARED表示共享模式
* @param return 返回创建的新结点
**/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//先在当前方法中用CAS进队试一次,不成功则进入enq()方法中反复尝试直到进队成功
Node pred = tail;
if (pred != null) {
node.prev = pred; //注意这里先置了node的前驱
if (compareAndSetTail(pred, node)) { //CAS操作尝试原子地将tail置为指向当前新建结点
pred.next = node; //成功说明tail已指向当前结点,则给当前结点前驱的next指针赋值
return node;
}
}
enq(node); //失败进入enq()方法反复尝试直到成功
return node;
}
先在当前方法中用CAS进队试一次,不成功则进入enq()方法中反复尝试直到进队成功
设置队尾节点流程
- 当前节点上继指向此时末尾节点
- 通过castail方法,将当前节点设置为末尾节点
- 设置成功后,将上个末尾节点的后继指向当前节点
如果设置失败的话进入enq进行反复重试
enq(Node)
private Node enq(final Node node) {
for (;;) { //经典“CAS + 失败重试”
Node t = tail;
if (t == null) { //需要初始化等待队列
if (compareAndSetHead(new Node()))
tail = head;
} else { //下面这部分和addWaiter方法中一样
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
CAS"自旋",直到成功加入队尾.
假如队列为空,初始化队列,因为肯定有头就会有尾 ,所以头点也是空的 ,创建一个空的标志结点作为head结点,并将tail也指向它
第二次循环的时候,就和addWaiter方法一样,设置队尾
例子
acquireQueued(Node, int)(线程阻塞等待获取资源)
通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了
源码
//已经进入等待队列的线程在队列中独占(且不响应中断)地获取锁的行为
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //获取失败标志,初值为true
try {
boolean interrupted = false; //记录线程在队列中获取锁的过程中是否发生过中断
//死循环,在循环中线程可能会被阻塞0次,1次,或多次,直到获取锁成功才跳出循环,方法返回
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);
}
}
这个方法呢就是进入等待队列的线程在队列中独占地获取锁的行为。
里面有个标准,判断是否发生过中断。
还是通过自旋方式获取锁,直到获取锁成功才跳出循环,方法返回
自旋 判断前继节点是不是头节点 当前节点是头节点点才会尝试获取锁 如果拿到锁后,把己设置成头节点,之前的头节点的后置节点为空,等待gc就可以了
如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
shouldParkAfterFailedAcquire(Node, Node)
此方法主要用于检查状态,看看自己是否真的可以去休息了
//等待队列中的线程不被允许获取锁或尝试获取锁失败后调用,检查自己是否可以阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //获取前驱的等待状态
if (ws == Node.SIGNAL) //前驱的等待状态已经是SIGNAL,则当前线程可以放心阻塞
return true; //表示要阻塞
if (ws > 0) { //前驱等待状态为CANCELLED,说明前驱已无效
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);//不断向前寻找状态不为CANCELLED的结点,同时将无效结点链成一个不可达的环,便于GC
pred.next = node; //找到状态不为CANCELLED的结点
} else {//前驱状态是PROGAGATE或0时,将其前驱的状态设为SIGNAL,在再次尝试失败后才阻塞(?)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false; //表示还要再尝试
}
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。
//线程阻塞,被唤醒时会返回是否是被中断唤醒的
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
看了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),现在让我们再回到acquireQueued(),总结下该函数的具体流程:
- 结点进入队尾后,检查状态,找到安全休息点;
- 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
- 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。
例子
流程图
release(int)
public final boolean release(int arg) {
if (tryRelease(arg)) { //尝试释放锁
Node h = head;
//如果head的waitStatus为0说明没有后继了,
//因为如果有后继,它的后继在阻塞前一定会把它的waitStatus设为SIGNAL
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //唤醒后继
return true;
}
return false;
}
调用tryRelease尝试释放锁,如果成功了,需要查看head的waitStatus状态,如果是0,表示CLH队列中没有后继节点了,不需要唤醒后继;
否则调用unparkSuccessor唤醒后继。而unparkSuccessor唤醒后继的原理是:找到node后面的第一个非1结点进行唤醒。
tryRelease(int)
此方法尝试去释放指定量的资源。下面是tryRelease()的源码:
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题
unparkSuccessor(Node)
//唤醒后继
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; //node是获取了锁的结点
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); //唤醒后继前,重置waitStatus为0
Node s = node.next; //node的next指针不一定指向其后继,当node的状态为cancelled的时候,其next指向自己
if (s == null || s.waitStatus > 0) { //这里的s == null的条件判断不理解(?)
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) //从后往前找node的后继中第一个没有被取消的结点
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //唤醒该结点线程
}
先将自己状态置为0,之后用unpark()唤醒等待队列中最前边的那个未放弃线程。
再和acquireQueued()联系起来,线程被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然线程已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,线程也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后线程把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!
Share(共享)
多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了。
核心方法 acquireShared--releaseShared
刚开始时,在共享资源允许范围内会有多个线程同时共享该锁,剩下的线程就被加入到CLH等待队列中排队阻塞等待;
当持有锁的线程释放锁时,它会唤醒在队列中等待的后继,而这个后继在获取锁之后会继续检查资源的剩余量,如果还有剩余,它会接着唤醒自己的后继。
也就是说,共享模式下,线程无论是在获取锁或者释放锁的时候,都可能会唤醒其后继,而且在共享资源允许的条件下会引起多个线程被连续唤醒。如果有多个线程同时获取了共享锁,则head指向的那个是CLH队列中最后一个持有锁的线程,其他的都已经出队了。
acquireShared(int arg)方法:AQS共享模式下获取锁的顶层入口
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //尝试获取锁,成功返回值大于等于0,失败返回值小于0
doAcquireShared(arg); //如果失败,则调用doAcquireShared方法获取锁
}
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); //将当前线程设为头,然后可能执行对后继SHARED结点的连续唤醒
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//获取锁失败,设置前驱waitStatus为SIGNAL,然后阻塞,这个过程与独占模式相同
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) //获取锁过程中发生异常造成未成功获取,则取消获取
cancelAcquire(node);
}
}
acquireQueued()很相似?对,其实流程并没有太大区别。只不过这里将补中断的selfInterrupt()放到doAcquireShared()里了,而独占模式是放到acquireQueued()之外,其实都一样
private void setHeadAndPropagate(Node node, int propagate) { //propagate是资源剩余量,从上面的调用中可以看到
Node h = head; //将旧的头结点先记录下来
setHead(node); //将当前node线程设为头结点,node已经获取了锁
//如果资源有剩余量,或者原来的头结点的waitStatus小于0,进一步检查node的后继是否也是共享模式
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
Node s = node.next; //得到node的后继
if (s == null || s.isShared()) //如果后继是共享模式或者现在还看不到后继的状态,则都继续唤醒后继线程
doReleaseShared();
}
}
其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后, 如果资源有剩余量,或者原来的头结点的waitStatus小于0,进一步检查node的后继是否也是共享模式 还会去唤醒后继队友的操作(这才是共享嘛)。
releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { //如果释放锁成功
doReleaseShared(); //启动对后继的持续唤醒
return true;
}
return false;
此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。
doReleaseShared
private void doReleaseShared() {
for (;;) {
Node h = head; //记录下当前的head
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { //如果head的waitStatus为SIGNAL,一定是它的后继设的,共享模式下要唤醒它的后继
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //先将head的waitStatus设置为0,成功后唤醒其后继
continue; // loop to recheck cases
unparkSuccessor(h); //关键,若成功唤醒了它的后继,它的后继就会去获取锁,如果获取成功,会造成head的改变
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //没有后继结点,设为PROPAGATE
continue; // loop on failed CAS
}
if (h == head) //若head发生改变,说明后继成功获取了锁,此时要检查新head的waitStatus,判断是否继续唤醒(下次循环)
break; //head没有发生改变则停止持续唤醒
}
}
独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。
ReentrantLock
ReentrantLock锁的实现基于AQS同步器:
只有一个实例变量sync提供锁的功能,在构造函数中初始化:
private final Sync sync;
sync有两种实例对象:FairSync(公平锁)和NofairSync(非公平锁),二者及其抽象类Sync都是继承于AQS以及RentrantLock的内部类。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
//获取锁成功则将当前线程标记为持有锁的线程,然后直接返回
setExclusiveOwnerThread(Thread.currentThread());
else
//获取锁失败则执行该方法
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程实例
int c = getState();//获取state变量的值,即当前锁被重入的次数
if (c == 0) { //state为0,说明当前锁未被任何线程持有
if (compareAndSetState(0, acquires)) { //以cas方式获取锁
setExclusiveOwnerThread(current); //将当前线程标记为持有锁的线程
return true;//获取锁成功,非重入
}
}
else if (current == getExclusiveOwnerThread()) { //当前线程就是持有锁的线程,说明该锁被重入了
int nextc = c + acquires;//计算state变量要更新的值
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//非同步方式更新state值
return true; //获取锁成功,重入
}
return false; //走到这里说明尝试获取锁失败
}
首先尝试快速获取锁,以cas的方式将state的值更新为1,只有当state的原值为0时更新才能成功,因为state在ReentrantLock的语境下等同于锁被线程重入的次数,这意味着只有当前锁未被任何线程持有时该动作才会返回成功。若获取锁成功,则将当前线程标记为持有锁的线程,然后整个加锁流程就结束了。若获取锁失败,则执行acquire方法
- 1.当前锁未被任何线程持有(state=0),则以cas方式获取锁,若获取成功则设置exclusiveOwnerThread为当前线程,然后返回成功的结果;若cas失败,说明在得到state=0和cas获取锁之间有其他线程已经获取了锁,返回失败结果。
- 2.若锁已经被当前线程获取(state>0,exclusiveOwnerThread为当前线程),则将锁的重入次数加1(state+1),然后返回成功结果。因为该线程之前已经获得了锁,所以这个累加操作不用同步。
- 3.若当前锁已经被其他线程持有(state>0,exclusiveOwnerThread不为当前线程),则直接返回失败结果
解锁
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) { //释放锁(state-1),若释放后锁可被其他线程获取(state=0),返回true
Node h = head;
//当前队列不为空且头结点状态不为初始化状态(0)
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //唤醒同步队列中被阻塞的线程
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //计算待更新的state值
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //待更新的state值为0,说明持有锁的线程未重入,一旦释放锁其他线程将能获取
free = true;
setExclusiveOwnerThread(null);//清除锁的持有线程标记
}
setState(c);//更新state值
return free;
}
tryRelease其实只是将线程持有锁的次数减1,即将state值减1,若减少后线程将完全释放锁(state值为0),则该方法将返回true,否则返回false。由于执行该方法的线程必然持有锁,故该方法不需要任何同步操作。
若当前线程已经完全释放锁,即锁可被其他线程使用,则还应该唤醒后续等待线程。不过在此之前需要进行两个条件的判断:
- h!=null是为了防止队列为空,即没有任何线程处于等待队列中,那么也就不需要进行唤醒的操作
- h.waitStatus != 0是为了防止队列中虽有线程,但该线程还未阻塞,由前面的分析知,线程在阻塞自己前必须设置前驱结点的状态为SIGNAL,否则它不会阻塞自己。
公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
}
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());
}
在真正CAS获取锁之前加了判断,从方法名我们就可知道这是判断队列中是否有优先级更高的等待线程,队列中哪个线程优先级最高?
由于头结点是当前获取锁的线程,队列中的第二个结点代表的线程优先级最高。
那么我们只要判断队列中第二个结点是否存在以及这个结点是否代表当前线程就行了
为什么FIFO的同步队列可以实现非公平锁
由FIFO队列的特性知,先加入同步队列等待的线程会比后加入的线程更靠近队列的头部,那么它将比后者更早的被唤醒,它也就能更早的得到锁。从这个意义上,对于在同步队列中等待的线程而言,它们获得锁的顺序和加入同步队列的顺序一致,这显然是一种公平模式。
然而,线程并非只有在加入队列后才有机会获得锁,哪怕同步队列中已有线程在等待,非公平锁的不公平之处就在于此。
回看下非公平锁的加锁流程,线程在进入同步队列等待之前有两次抢占锁的机会:
- 第一次是非重入式的获取锁,只有在当前锁未被任何线程占有(包括自身)时才能成功;
- 第二次是在进入同步队列前,包含所有情况的获取锁的方式。
只有这两次获取锁都失败后,线程才会构造结点并加入同步队列等待。而线程释放锁时是先释放锁(修改state值),然后才唤醒后继结点的线程的。试想下这种情况,线程A已经释放锁,但还没来得及唤醒后继线程C,而这时另一个线程B刚好尝试获取锁,此时锁恰好不被任何线程持有,它将成功获取锁而不用加入队列等待。线程C被唤醒尝试获取锁,而此时锁已经被线程B抢占,故而其获取失败并继续在队列中等待。整个过程如下图所示
如果以线程第一次尝试获取锁到最后成功获取锁的次序来看,非公平锁确实很不公平。因为在队列中等待很久的线程相比还未进入队列等待的线程并没有优先权,甚至竞争也处于劣势:在队列中的线程要等待其他线程唤醒,在获取锁之前还要检查前驱结点是否为头结点。在锁竞争激烈的情况下,在队列中等待的线程可能迟迟竞争不到锁。这也就非公平在高并发情况下会出现的饥饿问题。那我们再开发中为什么大多使用会导致饥饿的非公平锁?很简单,因为它性能好啊。
为什么非公平锁性能好
非公平锁对锁的竞争是抢占式的(队列中线程除外),线程在进入等待队列前可以进行两次尝试,这大大增加了获取锁的机会。这种好处体现在两个方面:
1.线程不必加入等待队列就可以获得锁,不仅免去了构造结点并加入队列的繁琐操作,同时也节省了线程阻塞唤醒的开销,线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用,是非常耗时的。在高并发情况下,如果线程持有锁的时间非常短,短到线程入队阻塞的过程超过线程持有并释放锁的时间开销,那么这种抢占式特性对并发性能的提升会更加明显。
2.减少CAS竞争。如果线程必须要加入阻塞队列才能获取锁,那入队时CAS竞争将变得异常激烈,CAS操作虽然不会导致失败线程挂起,但不断失败重试导致的对CPU的浪费也不能忽视。除此之外,加锁流程中至少有两处通过将某些特殊情况提前来减少CAS操作的竞争,增加并发情况下的性能。一处就是获取锁时将非重入的情况提前,如下图所示
CLH是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在节点之间的关系。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个节点)
里面的每个节点都是都是一个Node实体。其内部通过节点head和tail记录队首和队尾元素,队列元素的类型是Node。AQS通过获取state状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)加入CLH队列中,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒,再次同步状态。
入队
CLH队列入列就是tail指向新节点、新节点的prev指向当前最后的节点,当前最后一个节点的next指向当前节点。设置尾节点失败的话,调用CAS算法设置尾节点+死循环自旋,循环尝试,知道成功为止。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure(快速尝试添加尾节点)
Node pred = tail;
if (pred != null) {
node.prev = pred;
//CAS设置尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//多次尝试
enq(node);
return node;
}
private Node enq(final Node node) {
//死循环尝试,知道成功为止
for (;;) {
Node t = tail;
//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;
}
}
}
}
出队
首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点
rivate void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
Condition队列与CLH队列
使用synchronized控制同步时,可以配合Object的wait(),notify(),notifyAll()系列方法可以实现等待/通知模式。
使用Lock时,它提供了条件Condition接口,配合await(),signal(),signalAll()等方法也可以实现等待/通知机制。
Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
关系
- 线程1调用reentrantLock.lock时,线程被加入到AQS的等待队列中。
- 线程1调用await方法被调用时,该线程从AQS中移除,对应操作是锁的释放。接着马上被加入到Condition的等待队列中,等待着signal信号。
- 线程在某个ConditionObject对象上调用了singnal()方法后,等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。 注意,这个时候,线程1 并没有被唤醒。
- signal方法执行完毕,线程signalThread调用reentrantLock.unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,AQS释放锁后按从头到尾的顺序唤醒线程时,线程1被唤醒,于是线程1回复执行。
- 直到释放所整个过程执行完毕。
区别
- ConditionObject对象都维护了一个单独的等待队列 ,AQS所维护的CLH队列是同步队列,它们节点类型相同,都是Node。
AQS 对资源的共享方式
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
独占式
exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
acquire(int arg)是独占式获取同步状态的方法,我们来看一下源码:
public final void acquire(long arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- addWaiter方法
//构造Node
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure(快速尝试添加尾节点)
Node pred = tail;
if (pred != null) {
node.prev = pred;
//CAS设置尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//多次尝试
enq(node);
return node;
}
复制代码
- acquireQueued(final Node node, long arg)方法
final boolean acquireQueued(final Node node, long 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);
}
}
复制代码
- selfInterrupt()方法
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
共享式
多个线程可同时执行,如Semaphore/CountDownLatch等都是共享式的产物。
acquireShared(long arg)是共享式获取同步状态的方法,可以看一下源码:
public final void acquireShared(long arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
由上可得,先调用tryAcquireShared(int arg)方法尝试获取同步状态,如果获取失败,调用doAcquireShared(int arg)自旋方式获取同步状态
AQS的模板方法设计模式
模板方法模式:在一个方法中定义一个算法的骨架,而将一些步骤延申到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
就是AQS提供tryAcquire,tryAcquireShared等模板方法,给子类实现自定义的同步器。
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
- 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
- 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的
默认情况下,每个方法都抛出 UnsupportedOperationException
。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
AQS组件分析
ReentrantLock
ReentrantLock介绍
- ReentrantLock为重入锁,能够对共享资源能够重复加锁,是实现Lock接口的一个类。
- ReentrantLock支持公平锁和非公平锁两种方式
state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
CountDownLatch (倒计时器)
- CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
CyclicBarrier(循环栅栏)
CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
CLH 是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
CLH为什么用双向链表
AQS的双向链表,如果只是简单的加入节点与移除节点,其实使用单链表加上head、tail已经足够,与出队,入队没有关系。
在AQS中,还有其它一些功能。比如,线程A此时占有锁,线程B也来lock,因为A占有,B会判断是否需要阻塞,如下代码为判断是否需要阻塞,
//等待队列中的线程不被允许获取锁或尝试获取锁失败后调用,检查自己是否可以阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //获取前驱的等待状态
if (ws == Node.SIGNAL) //前驱的等待状态已经是SIGNAL,则当前线程可以放心阻塞
return true; //表示要阻塞
if (ws > 0) { //前驱等待状态为CANCELLED,说明前驱已无效
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);//不断向前寻找状态不为CANCELLED的结点,同时将无效结点链成一个不可达的环,便于GC
pred.next = node; //找到状态不为CANCELLED的结点
} else {//前驱状态是PROGAGATE或0时,将其前驱的状态设为SIGNAL,在再次尝试失败后才阻塞(?)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false; //表示还要再尝试
}