AQS是什么
AQS是AbstractQueuedSynchronizer的缩写,翻译过来就是"同步器",AbstractQueuedSynchronizer是一个抽象类,Java并包里大部分并发工具类都将其作为核心基础构件,比如可重入锁ReentrantLock, 信号量Semaphore基于各自的特点来调用AQS提供的基础能力方法实现多线程交互。例如:锁同步(synchronized)和锁等待(wait,notify)
AQS 中概念
AQS 按照获取同步状态的方式分为"独占式同步状态","共享式同步状态"
什么是独占式同步
概念上:一个时间点只能被一个线程占有的锁。
- 判断能否获取同步状态需要子类去实现模板方法tryAcquire,
- 判断能否释放同步状态需要子类去实现模板方法tryRelease
什么是共享式同步
概念上:能被多个线程同时占有锁。
- 判断能否获取同步状态需要子类去实现模板方法tryAcquireShared,
- 判断能否释放同步状态需要子类去实现模板方法tryReleaseShared
同步状态
AQS使用一个int类型的成员变量state来表示同步状态,AQS的实现类重写模板方法【参考AQS核心方法】,在其中运用此变量作为是否能够获取锁的依据。
同步队列
同步队列又被称为CLH队列,CLH队列是一个通过链式方式实现FIFO双向队列,AQS依赖它来完成同步状态的管理。当线程获取同步状态失败时,AQS则会将当前线程构造成一个节点(Node)并将其加入到CLH同步队列【节点会按照同步状态方式不同产生不同类型的节点】,同时会阻塞当前线程,当同步状态被释放时,会把首节点后第一个节点的线程从阻塞状态下唤醒,唤醒的线程会尝试竞争同步状态,如果能获取同步状态成功,则从同步队列中出队。
实例
public class LockDemo {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread = new Thread(() -> {
lock.lock();
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
thread.start();
}
}
}
实例代码中开启了5个线程,先获取锁之后再睡眠10S中,实际上这里让线程睡眠是想模拟出当线程无法获取锁时进入同步队列的情况。通过debug,当Thread-4(在本例中最后一个线程)获取锁失败后进入同步时,AQS时现在的同步队列如图所示
Condition & 等待队列
- Condition即"条件",是一个接口.它提供了和 Java 传统的监视器风格的 wait、notify、notifyAll 方法类似的功能的方法await(),signal(),signalAll()的方法.
- AQS内部存在一个内部类实现了Condition接口,通过一个链式方式实现单向等待队列实现了锁等待和锁释放的功能.并在此扩展出了超时等待,定时等待的功能的方法awaitNanos(),awaitUntil().
核心逻辑如下:
- 当获取同步状态的线程调用condition.await(),则会阻塞,并进入一个等待队列,释放同步状态.
- 当其他线程调用了condition.signal()方法,会从等待队列firstWaiter开始选择第一个等待状态不是取消的节点.添加到同步队列尾部.
- 当其他线程调用了condition.signalAll()方法,会从等待队列firstWaiter开始选择所有等待状态不是取消的节点.添加到同步队列尾部.
实例
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
});
thread.start();
}
}
新建了10个线程,每个线程先获取锁,然后调用condition.await方法释放锁将当前线程加入到等待队列中,通过debug控制当走到第10个线程的时候查看firstWaiter即等待队列中的头结点,debug模式下情景图如下:
再来看一个具体的condition的await,signal和object.wait(),notify()的对比实例:
package persistent.prestige.study.concurent.bread;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestMain {
public static void main(String[] args) {
BreadContainerByObject container = new BreadContainerByObject();
for (int i = 0; i < 5; i++) {
new Thread(new Producers(container)).start();
}
for (int i = 0; i < 3; i++) {
new Thread(new Customer(container)).start();
}
}
}
interface BreadContainer extends Serializable {
public void put(Bread b) throws InterruptedException;
public Bread poll() throws InterruptedException;
}
/**
* 基于 Reentrant Condition实现
* @author dingwei2
*
*/
@SuppressWarnings("serial")
class BreadContainerByCondition implements BreadContainer {
private Lock lock = new ReentrantLock();
private Condition NotFull = lock.newCondition();
private Condition NotEmpty = lock.newCondition();
// 面包容器
private List<Bread> breads = new ArrayList<Bread>();
private static final int MAX = 20;
private volatile int num = 0;
@Override
public void put(Bread b) throws InterruptedException {
// TODO Auto-generated method stub
try {
lock.lock();
while(breads.size() >= MAX ) { //已经满了
NotFull.await();
}
b.setId(num ++);
breads.add(b);
//放入一个元素后,NotEmpty
NotEmpty.signalAll();
} finally {
lock.unlock();
}
}
@Override
public Bread poll() throws InterruptedException{
try {
lock.lock();
while(breads.isEmpty()) {//如果为空
NotEmpty.await();
}
Bread b = breads.remove(breads.size() -1);
NotFull.signalAll();
return b;
} finally {
lock.unlock();
}
}
}
/**
* 基于 Object.notify Object.wait
* @author dingwei2
*
*/
@SuppressWarnings("serial")
class BreadContainerByObject implements BreadContainer{
// 面包容器
private List<Bread> breads = new ArrayList<Bread>();
private static final int MAX = 20;
private volatile int num = 0;
public void put(Bread b) {
synchronized (breads) {
while(breads.size() >= MAX) {
try {
breads.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();//这里不应该 将 InterruptedExcepiton 吞掉
}
}
b.setId(num ++);
breads.add(b);
breads.notifyAll();
}
}
public Bread poll() {
Bread b = null;
synchronized (breads) {
while(breads.size() < 1) {
try {
breads.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();//这里不应该 将 InterruptedExcepiton 吞掉
}
}
b = breads.remove(breads.size() -1);
breads.notifyAll();
}
return b;
}
}
/**
* 生产者
*
* @author dingwei2
*
*/
class Producers implements Runnable {
private BreadContainerByObject container;
public Producers(BreadContainerByObject container) {
this.container = container;
}
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 5; i++) {
Bread b = new Bread();
b.setFactoryName(Thread.currentThread().getName());
container.put(b);
}
}
}
/**
* 消费者
* @author dingwei2
*
*/
class Customer implements Runnable {
public BreadContainerByObject container;
public Customer(BreadContainerByObject container) {
this.container = container;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 5; i ++ ) {
Bread b = container.poll();
System.out.println(Thread.currentThread().getName() + "消费了" + b.toString());
}
}
}
@SuppressWarnings("serial")
class Bread implements Serializable {
private Integer id;
private String factoryName;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public String toString() {
return "面包:" + (id == null ? 0 : id.intValue()) + ";生产工厂:" + getFactoryName();
}
public String getFactoryName() {
return factoryName;
}
public void setFactoryName(String factoryName) {
this.factoryName = factoryName;
}
}
使用Reentrant Condition ,细化了消息通知的粒度,当队列中有产品可消费时,通过NotEmpty 条件来唤醒消费者,当队列还有可用的空间存放产品时,使用NotFull条件来唤醒生产者,使用两个条件队列,确保被唤醒的线程的准确性,加入到同步队列的节点,在该节点获取到锁后,确实是满足条件的(特别在临界情况的时候)。而Object.wait,Object.notify,生产者,消费者在同一个条件队列中排队。
CLH同步队列节点和等待队列节点
AQS 内部提供了一个内部类.用来作为同步队列和等待队列的节点对象.需要注意作为不同队列的节点.其使用的属性和含义是不同的.
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;
/**
* 单向等待队列节点时使用,等待节点需要被唤醒
*/
static final int CONDITION = -2;
/**
* 双向同步队列节点时使用,共享模式释放时,会将节点设置为此状态,并一直传播通知后续节点停止阻塞。尝试获取锁。
*/
static final int PROPAGATE = -3;
/** 等待状态 */
volatile int waitStatus;
/** 双向同步队列节点时使用,前置节点指针 */
volatile Node prev;
/** 双向同步队列节点时使用,后置节点指针 */
volatile Node next;
/** 获取同步状态的线程 */
volatile Thread thread;
/** 单项等待队列节点时使用,后置节点指针**/
Node nextWaiter;
//是否时CLH队列的节点同时时共享式获取同步状态
final boolean isShared() {
return nextWaiter == SHARED;
}
//获取当前节点的前置节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
//创建同步队列节点,node传入Node.SHARED或Node.EXCLUSIVE
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
//创建等待队列节点,waitStatus传入Node.CONDITION
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
AQS 实现原理
- AQS核心属性是一个int类型的成员变量state来表示同步状态,以及两个队列,CLH同步队列和等待队列构成。我们可以编写自己类继承AQS选择重写独占式或共享式模板方法,从而定义如何获取同步状态和释放同步状态的逻辑。无论独占式还时共享式获取同步状态成功则直接返回,失败则进入CLH同步队列并阻塞当前线程。当获取同步状态线程释放同步状态,AQS会选择从CLH队列head头部节点的第一个节点释放阻塞,尝试重写竞争获取同步状态,如果成功则将当前节点出队。如果失败则继续阻塞。
- 获取同步状态的线程也可以使用condition对象释放同步状态进入等待队列。只有等待其他线程使用condition.signal或condition.signAll()唤醒被从阻塞状态中释放重新竞争获取同步状态成功后从原来指令位置继续运行。
AQS核心方法
独占式获取同步状态
void acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功则直接返回,如果获取失败则线程阻塞,并插入同步队列进行.当调用release释放同步状态时,会从head头部后第一个节点中线程从阻塞中释放并在自旋中重新竞争同步状态,如果获取成功则出队
void acquireInterruptibly(int arg):独占式获取同步状态,与acquire方法相同,但在如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
boolean tryAcquireNanos(int arg, long nanosTimeout):独占式获取同步状态,与acquireInterruptibly方法相同,但在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false
boolean release(int arg):释放独占式同步状态,唤醒同步队列中首节点之后的第一个等待节点的线程的阻塞。
独占式获取同步状态模板方法
boolean tryAcquire(int arg):尝试独占式获取同步状态,返回值为true则表示获取成功,否则获取失败。
调用场景:
- 1 在acquire内部判断获取同步状态时调用
- 2 在release方法释放同步状态后,同步队列线程从阻塞中被唤醒,重新尝试获取同步状态时调用。
boolean tryRelease(int arg):尝试独占式释放同步状态,返回值为true则表示获取成功,否则获取失败。会
调用场景:
- 1 该方法在释放独占式同步状态【release方法】时
共享式获取同步状态
void acquireShared(int arg):共享式获取同步状态,如果当前线程获取同步状态成功则直接返回,如果获取失败则线程阻塞,并插入同步队列尾部。当调用releaseShared释放同步状态时,会找到从head头部节点后置节点中的线程,并将该线程从阻塞中释放。被释放的线程会在自旋中重新竞争同步状态。如果获取成功则出队,同时会判断新head节点【出队导致head节点变更】后置节点是否是共享节点,如果是轮询调用releaseShare直至某一个释放的节点尝试获取同步状态失败或同步队列不存在等待的线程则结束。
void acquireSharedInterruptibly(int arg):在acquireShared方法基础上增加了能响应中断的功能;
boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly基础上增加了超时等待的功能;
boolean releaseShared(int arg):释放共享式同步状态,释放共享式同步状态会唤醒同步队列中首节点之后的第一个等待节点的线程,并自旋中判断释放的节点是否获取了同步状态并出队,如果成功则继续释放新head节点之后一个等待节点的线程。直到某一个释放的线程获取同步状态失败
共享式获取同步状态模板方法
int tryAcquireShared(int arg): 尝试共享式获取同步状态,当返回值为大于等于0的时候方法结束说明获得成功获取锁,否则获取失败。
调用场景:
- 1 在acquireShared内部判断获取同步状态时调用
- 2 在releaseShared方法释放同步状态后,同步队列线程从阻塞中被唤醒,重新尝试获取同步状态时调用。
boolean tryReleaseShared(int arg):尝试共享式释放同步状态,返回值为true则表示获取成功,否则获取失败。
调用场景:
- 该方法在释放共享式同步状态【releaseShared方法】时会调用。【==这里要特别注意共享式释放具有传播性,如果此方法直接返回true会释放掉同步队列中所有的等待的线程==】
同步状态
getState():返回同步状态的当前值
setState(int newState):设置当前同步状态
compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性
同步队列
hasQueuedThreads():查询是否有任何线程正在等待获取。【在同步队列中是否存在等待线程】
int getQueueLength():返回等待获取的线程数的估计值.在同步队列中是否存在等待线程数量】
getQueuedThreads():返回包含可能等待获取的线程的集合。因为实际的线程集可能在构造此结果时动态地改变,所以返回的集合仅是尽力而为的估计值【返回同步队列中线程集合】
独占式 VS 共享式
从概念上来说独占式期望的是只有一个线程或者说竞争者获取同步状态,而共享式期望的是有多个线程或者竞争者能够获取同步状态。然而在实现上能否获取独占式同步状态和能否获取共享式同步状态是开放给子类自己去实现的。我们同样可以通过重写acquire(int arg)方法实现让多个线程获取独占式同步状态只是并没有这么做而已。那么他们的区别在哪?他们获取同步状态方式相同,获取失败进入阻塞队列同样相同。他们唯一的不同点在于共享式释放同步状态后,==唤醒同步队列中节点中的线程的阻塞,让唤醒线程去竞争同步状态这个动作具有具有传播性==
-
独占式在释放同步状态时,会找到从head头部后置节点中线程,使其从阻塞中释放,并在自旋中重新竞争同步状态,如果获取成功则出队。
-
共享式在释放同步状态时,会找到从head头部后置节点中线程,使其从阻塞中释放,并在自旋中重新竞争同步状态,如果获取成功则出队。同时会判断新head节点后置节点是否是共享节点,如果是则会再次释放同步状态。由于之前操作已经存在出队,此时head节点已经改变。依此重复上面操作:会找到从head头部后置节点中线程,使其从阻塞中释放,并在自旋中重新竞争同步状态,如果获取成功则出队,直至某一个释放的节点尝试获取同步状态失败或者同步队列已经不存在等待的节点结束。
AQS 白话
-
我们去餐馆吃饭,餐馆的座位是有限,如果餐馆只有一个座位那么获得座位的客人就是独占餐馆的资源也就是获得独占锁【tryAcquire作为判断】,如果有多个座位那么获得座位的多个客人获得就是共享锁【tryAcquireShared作为判断】,如果餐馆的座位满了,那么接下来怎么办,当然就是排队了,而这个队列就是CLH同步队列,每个人则是这个队列的节点Node,这个队列中每个人只能知道排在自己前面和后面的人(就是所谓的双向链式结构队列),让我们来模拟下这个第一位没有获得座位客户A的场景:
-
1 如果你是客人A,此时餐馆的座位已满,而你是第一个去排队的客人,那么餐馆的客服会让你去创建排队,客服是这个队列的head,而你是这个队列tail, 客服告知A去等通知(线程阻塞),并告知如果有位置会通知你(唤醒线程)
-
2 如果你是客户B, 此时餐馆的座位还是满的,且A在等待,餐馆的客服会告知你站在A的后面,并等通知(线程阻塞)
-
3 如果当前是独占式此时客户C就餐完毕离开,客服会通知A(唤醒线程),A被唤醒后会去餐馆找位位置【竞争同步状态】,如果成功则让A从同步队列中出队到此结束。
-
4 如果当前是共享式此时客户C就餐完毕离开,客服会通知A(唤醒线程),A被唤醒后回去餐馆找位位置【竞争同步状态】,如果成功则让A从同步队列中出队,此时A会通知B(唤醒线程),B被唤醒后回去餐馆找位位置【竞争同步状态】,如果成功则让B从同步队列中出队,B同样也会后面等待的客户,依此迭代传递直到某一个竞争同步状态失败。或者队列中无等待的节点则结束。
-
多说下这里获取同步状态在子类的实现中存在公平和公平获得座位的方式,因为每位客人来获取座位时都是先询问,如果这时候已经在排队了,而询问的时候正好有人离开,那么这么客人可以无视排队获得座位就是不公平的,相反只能去排队就是公平的。
作者:贪睡的企鹅
链接:https://www.jianshu.com/p/92568acbe5e6
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。