AQS
AQS
前言
前段时间菜鸡楼主面试,有好几个面试官问我
面试官:你觉得java中基础那些重要
我立马回答道:集合,多线程,锁
面试官:这样吧你说下锁相关的东西
我说道:有synchionzed和Lock接口,然后噼里啪啦的背着我的八股文
面试官:行了行了,你讲下aqs吧
我蒙蔽道:我不是很熟悉,但是知道一点点。然后讲了一下
然后就很尴尬,面试也草草的结束了
面试后觉得aqs很难,就是没去看,然后下一家又问,我又没回答出来,形成了一个恶性循环,因此在找到工作以后我决定好好看看学习。
在学习aqs前我们先简单看一下备用知识
1.备用知识
1.1LockSupport
看下面一段代码
public static void main(String[] args) throws InterruptedException {
Thread th = new Thread(() -> {
//阻塞当前线程
LockSupport.park();
System.out.println("子线程输出");
});
th.start();
Thread.sleep(2000);
System.out.println("main线程输出");
//释放th线程
LockSupport.unpark(th);
}
输出结果
main线程输出
子线程输出
其实LockSupport也就是线程之间通信的工具类,相对于传统Object方法中的notify和wait,更灵活,使用更方便。为什么说更灵活的,看下面这个例子
public static void main(String[] agrs) throws InterruptedException {
Thread th = new Thread(() -> {
//唤醒
LockSupport.unpark(Thread.currentThread());
//阻塞
LockSupport.park(Thread.currentThread());
System.out.println("子线程输出");
});
th.start();
Thread.sleep(2000);
System.out.println("main线程输出");
}
先唤醒在阻塞。这子线程不久一直阻塞死了吗?
子线程输出
main线程输出
答案是并没有阻塞,原因其实可以进入park方法阻塞看一看
/**
* Disables the current thread for thread scheduling purposes unless the
* permit is available.
*
* <p>If the permit is available then it is consumed and the call returns
* immediately; otherwise
* the current thread becomes disabled for thread scheduling
* purposes and lies dormant until one of three things happens:
*
* <ul>
* <li>Some other thread invokes {@link #unpark unpark} with the
* current thread as the target; or
*
* <li>Some other thread {@linkplain Thread#interrupt interrupts}
* the current thread; or
*
* <li>The call spuriously (that is, for no reason) returns.
* </ul>
*
* <p>This method does <em>not</em> report which of these caused the
* method to return. Callers should re-check the conditions which caused
* the thread to park in the first place. Callers may also determine,
* for example, the interrupt status of the thread upon return.
*
* @param blocker the synchronization object responsible for this
* thread parking
* @since 1.6
*/
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
第一段已经告诉你了阻塞当前线程除非有permit(许可证),所以相对LockSupport来说并没有严谨的执行顺序问题,而且使用起来也简单。
相对来说原理更多的是调用Unsafe方法,基本上也都是native方法,更多的也是C++实现的,也就不分析原理了。
1.2线程中断
线程中断即线程运行过程中被其他线程给打断了,它与 stop 最大的区别是:stop 是由系统强制终止线程,而线程中断则是给目标线程发送一个中断信号,如果目标线程没有接收线程中断的信号并结束线程,线程则不会终止,具体是否退出或者执行其他逻辑由目标线程决定。
其实按照我的理解就是stop方法直接强制结束一个线程非常不安全,所以在java中,每一个线程中都有一个中断标志(boolean类型),至于这个线程后续走向,你是想要他继续工作呢,还是想要他停止工作呢,就看你代码怎么写,怎么去处理。
然后接踵而来的就是和线程中断有关的的3个方法
- java.lang.Thread#interrupt
- java.lang.Thread#isInterrupted()
- java.lang.Thread#interrupted
看两个例子吧
public static void main(String[] agrs) throws InterruptedException {
Thread th = new Thread(() -> {
System.out.println("子线程中断状态:"+Thread.currentThread().isInterrupted());
LockSupport.park(Thread.currentThread());
System.out.println("子线程中断状态:"+Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();
System.out.println("子线程中断状态:"+Thread.currentThread().isInterrupted());
});
th.start();
Thread.sleep(5000);
th.interrupt();
}
子线程中断状态:false
子线程中断状态:true
子线程中断状态:true
public static void main(String[] agrs) throws InterruptedException {
Thread th = new Thread(() -> {
System.out.println("子线程中断状态:"+Thread.currentThread().isInterrupted());
LockSupport.park(Thread.currentThread());
System.out.println("子线程中断状态:"+Thread.currentThread().isInterrupted());
System.out.println("子线程中断状态:"+Thread.interrupted());
System.out.println("子线程中断状态:"+Thread.currentThread().isInterrupted());
});
th.start();
Thread.sleep(5000);
th.interrupt();
}
子线程中断状态:false
子线程中断状态:true
子线程中断状态:true
子线程中断状态:false
java.lang.Thread#isInterrupted()这个方法是用来判断线程中断的状态,相当get方法
java.lang.Thread#interrupt属于静态方法,他表示线程中断
java.lang.Thread#interrupted属于静态方法,获取当前线程的中断状态,但是会重置线程中断状态变成false
同时我们还发现一个问题,线程明明被park了,居然因为线程中断再次运行
所以也得出一个结论 唤醒被LockSupport.park())线程有2种办法
- 使用LockSupport.unpark()
- 使用线程中断
2.简介
AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。
个人理解:就是通过一个叫做CLH队列方式存储线程的行为状态,通过cas等各种方式来进行同步处理。
在java5之前,我们常用的线程同步方式都是采用synchronized关键字,并且它当时是一个重量级锁,使用起来效率不高,所以1.5之后就出现一个Lock接口搭配Aqs进行多个实现,相对来说使用的灵活性等都是很强的,虽然后续synchronized关键字有了一系列优化,但是在高并发情况下还是Lock接口性能稍微好点。
2.1同步状态
在AQS钟也就是private volatile int state 这个变量
这个变量代表的同步状态值,但是在不同的子类实现中也是不同含义,由实现着去实现
比如:
- ReentrantLock的state表示资源是否被锁定
- ReentrantReadWriteLock的state高16位代表读锁状态,低16位代表写锁状态
- 还有等等…(自行去看)
常用和state相关的方法
/**
*获取state
*/
protected final int getState() {
return state;
}
/**
*设置state
*/
protected final void setState(int newState) {
state = newState;
}
/**
*cas设置state
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2.2CLH队列
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
在AQS中具体的实现方式使用静态内部类Node来进行实现的,具体如下图
CLH队列
Condition队列
对于相应的字段解释
-
waitstatus
-
prev
指向前驱的节点
-
next
指向后驱的节点
-
thread
存放线程的信息
-
nextWaiter
在CLH队列时,nextWaiter表示共享式或独占式标记
在Condition队列中,nextWaiter表示前驱指针
2.3锁的分类
对于AQS本身是一个模板模式,我们只需要重写部分方法就可以实现,加上对上面Node分析,其实获取资源方式就是2种,共享锁和独占锁
对于独占锁
/**
*独占式获取资源,子类实现
*/
tryAcquire(int arg)
/**
*独占式释放资源,子类实现
*/
tryRelease(int arg)
/**
*独占式释放资源,实际获取调用
*/
acquire(int arg)
/**
*独占式释放资源,实际释放调用
*/
release(int arg)
对于共享锁
/**
*共享式获取资源,子类实现
*/
tryAcquireShared(int arg)
/**
*共享式释放资源,子类实现
*/
tryReleaseShared(int arg)
/**
*共享式获取资源,实际获取调用
*/
acquireShared(int arg)
/**
*共享式释放资源,实际释放调用
*/
releaseShared(int arg)
3.流程分析
3.1独占式获取流程
按照上述的模板方法,我们获取资源应该先去调用acquire(int arg);
public final void acquire(int arg) {
//1.尝试去调用我们子类重写的tryAcquire()去获取
//2.如果获取失败我们就去调用增加节点addWaiter()方法
//3.然后进行加入到队列acquireQueued()方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
对于第一步来说,是我们自己实现的获取资源
然后我们接着分析第二部做了什么
private Node addWaiter(Node mode) {
//创建一个Node节点
Node node = new Node(Thread.currentThread(), mode);
// 获取当前CLH队列的末尾节点
Node pred = tail;
//如果尾节点不是空,就cas把这个节点设置成为尾节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//进入end方法
enq(node);
return node;
}
我们找到了如果尾节点不是空,就cas加入到尾节点,但是如果尾节点是空呢?
所以就执行end()方法
private Node enq(final Node node) {
//无限制循环
for (;;) {
//获取尾节点
Node t = tail;
//如果尾节点是空,就说明CLH队列是空,cas设置CLH队列头结点和尾节点指向空节点
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
//说明尾节点不是空,cas加入到尾节点,然后返回
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
接下来就是对第一次入队的图分析
回到一开始的acquire方法,既然已经入队了,现在就执行第3步骤
final boolean acquireQueued(final Node node, int arg) {
//默认获取失败true
boolean failed = true;
try {
//默认线程是中断false
boolean interrupted = false;
//无限循环
for (;;) {
//获得当前节点的前驱节点
final Node p = node.predecessor();
//如果前驱节点是头结点,并且获取成功
if (p == head && tryAcquire(arg)) {
//设置当前节点为头结点
setHead(node);
//前驱节点设置为null,有利于GC
p.next = null;
failed = false;
return interrupted;
}
//看方法名字
//1-1.线程应该阻塞在获取资源失败
//1-2.阻塞线程然后检查中断状态
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//2-1.如果获取失败是true 就删除请求
if (failed)
cancelAcquire(node);
}
}
那么接下来看看1-1
/**
* @param 前驱节点
* @param 当前节点
* @return 是否线程应该被阻塞
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态值
int ws = pred.waitStatus;
//如果前驱状态值是SINGAL,代表当前节点已经设置前驱阶段,只需要等待前期节点的通知
if (ws == Node.SIGNAL)
return true;
//状态值>0的无非就是CANCELLED,也就是超时或者中断,也就是无效
if (ws > 0) {
//一直循环去获取前驱节点的前驱节点,直到它的状态值不是>0
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
//前驱节点是0或者PROPAGATE,然后去cas设置前驱节点状态为SIGNAL
} 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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
在来看1-2
private final boolean parkAndCheckInterrupt() {
//阻塞当前线程
LockSupport.park(this);
//获取当前线程中断状态,并且设置当前线程中断状态为false
return Thread.interrupted();
}
再来看2-1
private void cancelAcquire(Node node) {
//判断空校验
if (node == null)
return;
node.thread = null;
//获取前驱节点
Node pred = node.prev;
//获取前驱节点状态值不>0的节点为前驱节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//获取真正的有效前驱节点的后驱节点(可能是自己)
Node predNext = pred.next;
//设置当前节点状态值为CANCELLED
node.waitStatus = Node.CANCELLED;
//如果我们是尾节点,就删除自己
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
//1.前驱不是头结点,并且前驱节点线程不是空(公有条件)
//2.前驱节点是SIGNAL或者是<0状态(共享式情况下)cas设置SIGNAL
//cas把当前节点的后续节点提上来设置在有效前驱节点的后面
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 {
//3-1唤醒线程
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
3-1唤醒线程
private void unparkSuccessor(Node node) {
//cas设置当前节点为0
int ws = node.waitStatus;
if (ws < 0)
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);
}
总结
大致流程
cancelAcquire
3.2独占式释放流程
public final boolean release(long arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
没啥好说的… 通过unpark()去唤醒
下面给段代码自行debug走aqs流程
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Consumer<Integer> consumer = (i)->{
lock.lock();
try {
System.out.println("线程"+i+"占用了");
}finally {
}
};
new Thread(()->consumer.accept(1)).start();
Thread thread2 = new Thread(() -> consumer.accept(2));
thread2.start();
thread2.join()
}
3.3共享式获取释放流程
主要还是关注acquireShared(int arg)方法
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared()方法是我们子类实现的,大致上是获取资源,返回当前剩余的资源
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);
//如果剩余资源>=0
if (r >= 0) {
//1-1设置头结点,并且设置后续共享传播
//这里我纠结挺久的
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//这边和独占式一样所以不说了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
整个流程我有疑问的地方就是setHeadAndPropagate方法
private void setHeadAndPropagate(Node node, int propagate) {
//保存当前头结点
Node h = head; // Record old head for check below
//设置当前头结点
setHead(node);
//如果剩余资源>0
//或者当前头结点不是null,并且状态<0
//或者之前头结点不是null,并且状态<0
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
//获取当前节点后驱节点
Node s = node.next;
//如果是NULL或者共享节点
if (s == null || s.isShared())
//去释放
doReleaseShared();
}
}
在看释放过程
private void doReleaseShared() {
for (;;) {
//获取头结点
Node h = head;
//如果头结点!=null 并且 头结点不是尾节点
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果头结点是single状态
if (ws == Node.SIGNAL) {
//cas设置当前节点是0
//因为这里有并发,所有重试
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//唤醒
unparkSuccessor(h);
}
//如果头结点是0并且cas设置PROPAGATE失败,就重试
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//h==头结点就返回了
if (h == head)
break;
}
}
4.案例
4.1独占锁ReentrantLock
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
/**
*抽象类锁
**/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
//非公平获取资源
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前资源数
int c = getState();
//如果资源数==0
if (c == 0) {
//cas设置资源数
if (compareAndSetState(0, acquires)) {
//设置当前独占锁
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0);
}
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
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 void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
}