一、队列同步器AQS
AQS是一个为满足同步需求、保障线程安全,构建锁或者其他同步组件的框架或者叫模板、底层实现。我们用它可以实现我们自己像要的同步组件(就像ReentrantLock)。AQS是一个抽象类,使用者需要自定义一个同步器并且继承它,重写它指定的方法(独占式/共享式获取/释放同步状态方法),再将自定义同步器组合在自己定义的同步组件中,当真正用的时候同步组件调自定义同步器的模板方法,模板方法再调我们重写的方法,从设计模式角度来看这是一个很典型的【模板模式】
它与锁的区别:
锁是应用层,是给我们开发使用的,封装好了线程同步的逻辑,定义了使用者与锁的接口交互;
AQS是实现层,是锁的实现者,是锁的内部实现。
二、框架
AQS内部维护了一个int类型的volatile变量state(同步状态)和一个先进先出的虚拟双向队列,这两个很重要,可以说是实现同步功能的核心。
AQS对共享资源定义了两种访问形式:独占式(Exclusive,如:ReentrantLock)和共享式(Share,如:CountDownLatch/Semaphore)
自定义同步器需要重写的方法:
boolean tryAcquire(int) | 独占式,尝试获取资源,非阻塞立即返回结果 |
---|---|
boolan tryRelease(int) | 独占式,尝试释放资源,非阻塞立即返回结果 |
int tryAcquireShared(int) | 共享式,尝试获取资源,非阻塞,返回结果 <0获取失败;>0获取成功,还有多余资源;=0获取成功,资源正好,没有多余资源 |
boolan tryReleaseShared(int) | 共享式,尝试释放资源,非阻塞,返回结果 true表示唤醒后继等待节点,否则false |
isHeldExclusively() | 该线程是否正在独占资源 |
AQS提供的模板方法(主要功能:调用以上重写的方法,把获取资源失败的线程加入等待队列)
final void acquire(int arg) | 独占式获取同步状态,获取失败进入同步等待队列 |
---|---|
final void acquireInterruptibly(int arg) | 独占式,能响应中断,获取进入等待队列,如被中断,抛出中断异常并返回 |
final boolean tryAcquireNanos(int,long) | 独占式,支持超时,限定时间内获取则返回true,否则返回false |
final boolean release(int arg) | 独占式,释放同步状态,返回释放是否成功 |
final void acquireShared(int arg) | 共享式获取同步状态,失败进入等待队列。同一时间可以支持多线程获取 |
final void acquireSharedInterruptibly(int ) | 共享式获取,能响应中断 |
final boolean tryAcquireSharedNanos | 共享式获取,支持超时 |
final boolean releaseShared(int arg) | 共享式,释放同步状态 |
AQS里有两个内部类:Node和Condition。Node很重要,是等待队列的基础,整个同步过程就是围绕Node和state展开的。
Node:构成等待队列的节点,获取资源失败的线程会被封装成一个node,然后加入到等待队列。node包含了线程本身和等待状态waitState,有5中等待状态。
Condition:主要用在同步组件中实现线程通讯,类似于synchronized锁代码块的时候有等待/通知机制(wait/notify),lock也有啊,lock的等待通知机制实现就是condition,并且他比wait/notify更强大,它可以创建多个,实现多个线程之间交叉相互通信。
Node的5中等待状态(理解这5个状态对后边的源码分析很有帮助):
CANCELLED(1) | 节点已被取消,或者是超时或者是被中断,反正是不再需要获取资源 |
---|---|
SIGNAL(-1) | 节点是这个状态代表它释放资源后要唤醒后继节点,新节点加入队列时,会把新节点的前继节点状态改为Signal,自己就去挂起休息了,等着被通知。这个状态很重要,理解了对后面很有帮助 |
CONDITION(-2) | 结点等待在Condition上,当调用了Condition的signal()方法后,节点就从等待队列进入到阻塞队列,等待获取同步资源 |
PROPAGATE(-3) | 共享模式下,前继结点会唤醒后继结点,并一直往后唤醒,传播下去 |
0 | 无含义,默认状态,刚创建的新节点状态 |
三、应用
模仿ReenactmentLock写一个自己的互斥锁,主要是看怎么实现怎么使用的
package com.bbwt.thread.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/***
* 自定义一个同步组件(互斥锁)
* 假设只提供一个同步资源:state是1
* 例子式独占式的
*/
public class Mutex implements Lock {
//自己定义一个同步器,这是一个静态内部类,继承自AQS,需要重写独占式的方法
private static class Sync extends AbstractQueuedSynchronizer {
//重写方法:当前同步状态是否被占用
@Override
protected boolean isHeldExclusively(){
//如果state是1就是被当前线程独占,其他线程拿不到了需要等待
return getState()==1;
}
//重写方法:尝试获取同步资源,独占式
@Override
public boolean tryAcquire(int acquires){
//采用cas设置state状态
boolean b = compareAndSetState(0, acquires);
if(b){
//如果设置成功,代表获取了共享资源,当前线程占有同步状态
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//重写方法:尝试释放同步状态,独占式
@Override
protected boolean tryRelease(int release){
if(getState()==0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
Condition newCondition (){
return new ConditionObject();
}
}
//自己不干,代理给sync,因为sync是自定义得同步器,实现了自己想要得功能
private final Sync sync = new Sync();
/***
* 调同步器的模板方法,模板方法再调重写的拿锁方法
* 该方法一定会拿到同步状态,阻塞式
*/
@Override
public void lock() {
sync.acquire(1);
}
/***
* 尝试获取同步状态,非阻塞式,立即响应结果:拿到还是没拿到
* @return 是否获取成功
*/
public boolean tryLock(){
return sync.tryAcquire(1);
}
/***
* lock()和tryLock()都是独占式获取同步状态,都是调tryAcquire()
* lock是先拿一次,拿不到就一直循环拿,直到能拿到,所以是阻塞式
* tryLock是就拿一次,拿到拿不到都返回,所以是非阻塞式
*/
/***
* 释放同步状态
* 跟上面获取是一个道理
*/
@Override
public void unlock() {
sync.release(0);
}
public boolean tryUnlock() {
return sync.tryRelease(0);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
//使用
public static void main(String[] args) {
Mutex mutex1 = new Mutex();//创建自己的互斥锁
Thread t = new Thread(new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
mutex1.lock();
try {
System.out.println(name+":加锁成功,执行自己的逻辑");
Thread.sleep(2000);
}catch (Exception e){
}finally {
mutex1.unlock();
}
System.out.println(name+":释放锁");
}
},"Thread-1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
mutex1.lock();
try {
System.out.println(name+":加锁成功,执行自己的逻辑");
Thread.sleep(2000);
}catch (Exception e){
}finally {
mutex1.unlock();
}
System.out.println(name+":释放锁");
}
},"Thread-2");
t.start();
t2.start();
}
}
四、实现分析
4.1 、实现原理
AQS 内部维护了一个同步队列,用于管理同步状态:
-
当线程获取同步状态失败时,就会将当前线程以及等待状态等信息构造成一个 Node 节点,将其加入到同步队列中尾部,阻塞该线程
-
当同步状态被释放时,会唤醒同步队列中“首节点”的线程获取同步状态
4.2 、源码分析
独占式:
关于源码理解分三步:
1、先尝试获取同步状态,调用重写的独占式获取方法。
2、获取失败后就构建node节点,安全的加入到队列的尾部。
3、忙循环方式一直等到获取同步状态。这一步主要工作就是循环方式给前驱节点下通知,通知成功后,该线程就暂停了,也不循环了,等前驱节点通知自己后再在循环里尝试获取同步状态
/***
* 模板方法 需要在自定义方法里调用该模板方法
*/
public final void acquire(int arg) {
//① 尝试拿同步状态
boolean ok = tryAcquire(arg);
//② 获取失败的线程构建成node等待节点,安全的加入到队列尾部
if(!ok){
Node node = new Node(Thread.currentThread(), Node.EXCLUSIVE);
Node pred = tail;
boolean setTail = true;//标记设置成尾节点的状态
//当队列不为空时才尝试加入到尾部
if (pred != null && compareAndSetTail(pred, node)) {
node.prev = pred;
pred.next = node;
setTail = false;//成功的加入到尾部了
}
//队列里一个节点都没有或者并发时没有成功加入尾部
if(setTail){
node.prev = pred;
//采用循环的方式设置尾节点,结束条件是:node成功的设置成尾节点
for (;;) {
Node t = tail;//每次循环都重新取一下尾节点
//还没有尾节点。可以理解成空队列
if (t == null) {
//建一个空节点,既是头又是尾,cas原子操作,保证多线程下只构建一个哨兵节点,有个哨兵节点后面的操作就会避免空指针
if (compareAndSetHead(new Node())) {
tail = head;//头和尾都是该哨兵节点,再次循环队列就不为空了
}
} else {
//队列不为空就尝试设置成尾节点,设置不成功就循环,直到成功设置
if (compareAndSetTail(t, node)) {
node.prev = t;
t.next = node;
break;
}
}
}
}
//经过第二部操作,等待的线程已经成功加入到队列了,下面就该等着获取同步状态了
//③ 等待最终获取到同步状态
boolean failed = true;//标记整个流程是否出问题了
boolean interrupted = false;//标记是否被中断过
try {
//忙循环设置前驱节点状态(给前驱节点下个通知:你用完了同步状态告诉我,我再用)
for (;;) {
final Node p = node.predecessor();//找到前驱节点
//当前驱是头节点时就尝试再拿一次同步状态
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // 断开之前那个头节点(可能是哨兵节点),有利于gc回收
failed = false;
break;//获取成功后就跳出循环
}
//没有拿到同步状态就给前驱节点一个通知(设置成SIGNAL,表示自己用完同步状态后要通知后继节点),自己就挂起了(暂停状态)
int ws = pred.waitStatus;//拿到前驱线程的状态
if (ws == Node.SIGNAL){//如果前驱节点已经是SIGNAL状态了,没自己啥事可以被中断了,等着被unpark唤醒就行了。
// 同步状态释放的时候会有唤醒操作,唤醒后会继续这个循环,有机会获取同步状态了
LockSupport.park(this);//被park后就不在走下面了,直到被unpark
if(Thread.interrupted()){
interrupted = true;//标记成被中断了
}
}else if (ws > 0) {
//剔除被取消了的节点(这些节点可能因为中断或者超时等原因不再争夺同步状态了)
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);//一直往前找,找到正常的(signal)的节点
pred.next = node;
} else {
//如果前驱节点正常并且不是SIGNAL状态就用cas安全的把前驱节点设置成SIGNAL状态
compareAndSetWaitStatus(pred, ws, AbstractQueuedSynchronizerMini.Node.SIGNAL);
}
//节点的状态这块大体意思就是先看看是不是想要的状态,是:就挂起;不是再判断是不是被取消了,没取消就设置signal状态
//经过这个try块后最终会获取到同步状态
}
} finally {
//如果在没成功获取同步状态之前抛异常了
if (failed) {
//取消等待的这个节点,并且从这个尾节点往上找,找到第一个正常的节点设置成尾节点
cancelAcquire(node);
}
}
if(interrupted){
//如果它被要求中断过,自己再主动中断
Thread.currentThread().interrupt();
}
}
}
```java
/***
* 队列同步器提供的:模板方法 需要在自定义方法里调用该模板方法
* 独占式释放同步状态,主要分2步:
* 1、调用重写的方法释放同步状态
* 2、把自己节点状态设置为0,用unpark唤醒后继节点(等待队列中最靠前的那个正常节点)
* @param arg
*/
public final boolean release(int arg) {
//调用子类重写的尝试获取同步状态的方法
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0){
// unparkSuccessor(h); 以下就是该方法
int ws = h.waitStatus;
if (ws < 0){//节点是正常状态
compareAndSetWaitStatus(h, ws, 0);//释放同步状态了,就把该节点的状态设置成0
}
Node s = h.next;//后继节点
//如果没有后继节点或者后继节点状态是取消了,从后往前找,直到找到后面的第一个正常的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != h; t = t.prev) {
if (t.waitStatus <= 0) {
s = t;
}
}
}
if (s != null) {
LockSupport.unpark(s.thread);//唤醒后继节点
}
return true;
}
}
return false;
}
共享式:
/***
* 队列同步器提供的:模板方法 需要在自定义的方法里调用该模板方法,然后它在调用重写的同步器方法tryAcquireShared()
* 共享式获取同步状态,整体过程分2步:
* 1、尝试获取同步状态;
* 2、获取失败就加入等待队列
* @param arg
*/
public final void acquireShared(int arg) {
//先判断是否成功获取了同步状态
int state = tryAcquireShared(arg);
if ( state< 0){
//没有拿到同步状态,加入等待队列
doAcquireShared(arg);
}
}
/***
* 共享式获取同步状态失败进入等待队列
* @param arg
*/
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);
p.next = null; // help GC
//如果被标记为中断的,则可以进行线程中断了
if (interrupted) {
selfInterrupt();
}
failed = false;
return;
}
}
//这两个方法跟独占式的功能一样,都是把前驱节点标记为Signal后自己挂起线程去休息,等着被通知就行了
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/***
* 设置头节点并传播下去
* @param node 该节点是曾经的老二
* @param propagate
*/
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);//曾经的老二现在是扛把子了
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared()) {//如果后继节点是共享的,告诉后继节点它也可以尝试去拿同步状态了
doReleaseShared();
}
}
}
/***
* 队列同步器提供的:模板方法 需要在自定义方法里调用该模板方法
* 共享式释放同步状态,主要分2步:
* 1、调用重写的方法释放同步状态
* 2、用unpark唤醒后继节点(等待队列中最靠前的那个正常节点)
* @param arg
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
/***
* 共享模式释放同步状态 并且 往后传播下去(告诉后面得已经释放同步状态了)
*/
private void doReleaseShared() {
for (;;) {
Node h = head;
//存在头节点并且它还有后继节点
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果头节点是SIGNAL状态,它需要唤醒后继节点了
if (ws == Node.SIGNAL) {
//把自己节点状态设置成0,如果设置失败就跳出本次循环,下一次循环在尝试设置
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
continue;
}
unparkSuccessor(h);//自己完全“没用”了,唤醒后继节点
}
else if (ws == 0 && !compareAndSetWaitStatus(h,0,Node.PROPAGATE)) {
continue; // loop on failed CAS
}
}
if (h == head) {
break;
}
}
}