AQS(AbstractQueuedSynchronizer) :抽象的队列同步器
技术解释:是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列(先进先出队列)来完成资源获取和线程排队的工作,并通过一个int类型变量表示持有锁的状态。
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH:Craing、Landin and Hagersten队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIF、
在并发场景中,有的线程获取不到锁,就需要进入阻塞状态,而AQS就是一个队列来管理这里线程的。以ReentrantLock为例,探讨一下AQS。
AQS(AbstractQueuedSynchronizer)这个抽象类中有一个内部类Node,用来封装阻塞线程的。
volatile Node prev;//指向前一个节点
volatile Node next;//指向后一个节点
private transient volatile Node head;//头结点指针
private transient volatile Node tail;//尾节点指针
private volatile int state;//表示同步状态
volatile Thread thread;//节点保存的线程
模仿用户去办理业务,有三个人,顾客A,顾客B,顾客C。顾客A办理业务时间长,顾客B、C都会等待。
package com.yun.AQSDemo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//带入一个银行办理业务的案例来模拟我们的AQS 如何进行线程的管理和通知唤醒机制
//3个线程模拟3个来银行网点,受理窗口办理业务的顾客
//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " come in.");
try {
TimeUnit.SECONDS.sleep(60);//模拟办理业务时间
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}, "Thread A").start();
//第2个顾客,第2个线程->,由于受理业务的窗口只有一个(只能一个线程持有锁),此代B只能待
//进入候客区
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " come in.");
} finally {
lock.unlock();
}
}, "Thread B").start();
//第3个顾客,第3个线程->,由于受理业务的窗口只有一个(只能一个线程持有锁),此代C只能待
//进入候客区
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " come in.");
} finally {
lock.unlock();
}
}, "Thread C").start();
}
}
ReentrantLock类中有三个内部类,一个是sync,一个是FairSync,另一个是NonfairSync。其中FairSync和NonfairSync都是继承的sync,sync继承的是AQS(AbstractQueuedSynchronizer) 。
执行ReentrantLock lock = new ReentrantLock();其实底层就是创建一个sync,默认创建的是非公平的锁
当顾客A先调用lock的时候,其实底层调用的是sync的lock方法。ync的lock方法是一个抽象方法,子类必须实现改方法。由于默认创建的是非公平的锁,因此这里只看NonfairSync重的lock方法。
compareAndSetState(0, 1)方法就是一个CAS的思想,代表期望当前共享资源的占用状态是0,那么就修改占用状态为1(0表示当前共享资源没有人占用,1表示被占用),并且设置当前线程独占访问权限。
第一次这个共享资源没有被线程占用,所以可以设置当前线程为工作线程,后面来的线程通过 compareAndSetState(0, 1)方法试图修改线程占用当前资源,但是占用不了,返回false,就会调用acquire()方法。因此顾客B进来会走acquire()方法。
这个方法里面包含了三个方法。其中当前线程还会通过tryAcquire()去尝试抢占一下资源,
tryAcquire()方法中就只有一个抛出异常,这是一个典型的设计模式中的模板设计模式。这个方法是AQS(AbstractQueuedSynchronizer)中的方法,其子类都实现了这个方法的。就看NonfairSync子类中的实现方法。
在NonfairSync类中,该方法调用的是非公平的tryAcquire()方法。
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程,也就是顾客B
final Thread current = Thread.currentThread();
//获取当前资源占有状态
//当前有资源被占用了,所以是1
int c = getState();
//这种情况是,刚好顾客A释放了资源,然后刚好顾客B来抢占资源
//正好抢占到了,就会调用compareAndSetState(0, acquires)
//也是CAS,修改当前资源类的占用状态,并且设置当前线程为资源占用线程
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//getExclusiveOwnerThread()获取当前占用资源的线程
//判断当前进来的线程和当前占用资源的线程是不是同一个
//这里相当于顾客A刚刚办完业务,但是又想重新办理业务
//相当于重入锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//返回false,抢占失败
return false;
}
如果抢占失败,返回false,然后取反就是true,相当于顾客B没有抢到锁,就需要入队了,就会调用addWaiter()方法
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
//将当前线程,也就是顾客B封装成一个Node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//由于现在线程B还没有加进来,现在这个队列其实没有节点
//所以头结点指针和尾节点指针都是null
Node pred = tail;
//由于不满足这个条件,现在B走的是enq这个方法
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
//这个for循环相当于while(true){}的自旋
for (;;) {
//由于队列中没有任何节点
//尾指针为null,因此t==null
//第一次循环,走的是这里,创建了第一个节点,也就是伪节点后,下一次循环就走else
Node t = tail;
if (t == null) { // Must initialize
//队列中没有节点,会先初始化一个节点,相当于伪节点
//作用就是用来占位的
//因此线程B不是队列中的第一个节点
if (compareAndSetHead(new Node()))
//compareAndSetHead(new Node()这个方法也是用的CAS,希望当前位置的节点为null
//然后更新为一个新建的节点,也就是伪节点,并且将头结点指向新建的节点
//把尾节点指向头结点指向的节点
tail = head;
} else {
//第二次循环的时候,因为队列中有一个节点了
//node封装了真正的线程B
//先将node的前指针指向当前队列中的最后一个节点
node.prev = t;
//然后在通过CAS将尾节点指针指向node
if (compareAndSetTail(t, node)) {
//最后再将之前尾节点的next指向node
t.next = node;
//返回t
return t;
}
}
}
}
当队列中创建了这个伪节点后
把封装线程B的node节点加入队列后
此时线程B就已经入队了。
线程C入队的时候,因为队列中已经有节点了,因此在addWaiter(Node mode) 方法中,直接会把封装线程C的节点添加进队列,不用走enq方法。
此时B、C入队。这时候就会进入acquireQueued(final Node node, int arg)方法
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋
for (;;) {
//获取当前节点的前一个结点
final Node p = node.predecessor();
//判断一下该节点的前一个节点是不是头结点
//其实就是判断一下当前节点是不是第一个有数据的节点,先进先出
//并且尝试去抢占一下资源ryAcquire(arg)
//当线程A不占用资源,并且调用unpark方法后
//线程B被唤醒
//这是一个自旋方法,线程B又会通过tryAcquire(arg)方法去尝试抢占资源
//这次确实就能抢占到资源了
if (p == head && tryAcquire(arg)) {
//setHead将封装线程B的节点设置为头结点
//然后再将头指针指向节点B
//设置节点中封装的线程为null
//节点的前指针为null
//此时当前节点就相当于是伪节点了,线程B已经去占用资源了
setHead(node);
/**
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
*/
//这里相当于让之前的伪节点没有任何指向,然后就会被垃圾回收器回收
p.next = null; // help GC
failed = false;
return interrupted;
}
//抢占资源失败后,会调用这个方法shouldParkAfterFailedAcquire(p, node)
//这个方法中,先获取当前节点的前一个节点的waitStatus
//最开始waitStatus为0
//然后判断waitStatus为不为-1,>0?
//都不满足,然后用CAS把waitStatus从0改为-1
//由于这是一个自旋,第二次循环的时候,waitStatus==-1,该方法返回true
//因此会进入 parkAndCheckInterrupt()方法
//p是当前节点的前一个节点,node是当前节点
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//最开始waitStatus都是0
int ws = pred.waitStatus;
//SIGNAL==-1
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//CAS,将pred节点的waitStatus设置为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
//调用LockSupport.park让线程进入阻塞状态
//此时可以认为线程B真正入队了
//这里会一直进行阻塞,直到调用unpark方法。
LockSupport.park(this);
//返回线程中断信息
//当调用了unpark方法后,线程B不会阻塞,因此返回false
return Thread.interrupted();
}
到这里之后,线程B和C才算是真正的入队了
此时线程A办理完业务,准备释放资源,调用unlock方法。unlock方法底层调用的sync.release(1)
public final boolean release(int arg) {
//调用tryRelease释放锁,将资源的占用线程设置为null,返回true
if (tryRelease(arg)) {
//h就是头结点,也就是伪节点
Node h = head;
//因为之前已经将队列中的所有节点的waitStatus 设置为了-1
//因此会进入unparkSuccessor(h)方法
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release方法中调用tryRelease方法
tryRelease也是设计模式中的模板设计方法,跑出一个异常,让子类去实现这个方法,这次走的是ReentrantLock
protected final boolean tryRelease(int releases) {
//当前线程的state为1,传进来的releases也是1,因此c==0
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
//设置当当前资源的占用线程为null
setExclusiveOwnerThread(null);
}
//设置当前state的状态为0,表示没有线程占用这个资源
setState(c);
//返回true
return free;
}
private void unparkSuccessor(Node node) {
//传进来的node节点就是伪节点,伪节点的waitStatus==-1
int ws = node.waitStatus;
//满足这个条件
if (ws < 0)
//用CAS将伪节点的waitStatus设置为00
compareAndSetWaitStatus(node, ws, 0);
//s节点就是第一个有内容的节点,也就是B节点
Node s = node.next;
//B节点不为null,并且B节点的waitStatus为-1
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;
}
//B节点不为null
if (s != null)
//调用unpark唤醒B节点
LockSupport.unpark(s.thread);
}
当线程A释放资源,会调用unpark方法,之前线程B和线程C都调用了park方法进行阻塞,线程A调用了unpark方法后就会唤醒线程B。因此在parkAndCheckInterrupt方法中会返回false。
parkAndCheckInterrupt返回false之后,因为acquireQueued方法是自旋,会再一次进行循环,这次线程B就会通过tryAcquire方法抢占到资源,然后再将节点B设置为头结点,里面的线程设为null,并且取消与之前的伪节点的联系,让节点B成为新的伪节点,之前的伪节点被GC回收。
=========================================================================
其他:
相当于在非公平锁下,新加进来的现在在入队之前最少都要尝试抢占4次资源
第一次是在刚调用sync中的lock方法锁定时,会尝试抢一次
第二次是在acquire方法中的tryAcquire方法中尝试抢占一次
第三、四次会在acquireQueued方法中抢占,因为acquireQueued是自旋,第一次循环的时候会抢占一次,并且设置当前节点的前一个节点的waitStatus==-1,第二次循环的时候再抢占一次,然后在调用park方法将当前线程进行阻塞