一 ,概述
AQS 全名 AbstractQueuedSynchronizer 是juc 包下的一个 抽象类 也叫抽象队列同步器 是用来实现 同步器(也就是锁)的一个 解决方案, 平时开发中用到的juc下的那些东西比如ReentrantLock ,CountDownLatch 还有个读写锁的那种 都是通过实现AQS 来实现的 。
二, 原理
线程执行lock 方法的时候呢 会通过cas 方法 替换一个 状态位 如果替换成功那就 拿锁成功, 执行代码。如果替换失败(队列同步器
队列同步器 那玩意说白了肯定是有个队列的) 进入到队列里面 然后 直接把线程挂起。 等着吧 等着拿到锁的线程释放锁的时候 从队列里拿到
等待的线程 然后唤醒就ok。 嗯大概就这样。 下面看下这些要素在源码里对应的属性把。
AbstractQueuedSynchronizer 类中重要的属性
//锁状态位 这个就是上面说的 那个cas 替换的东西。
private volatile int state;
//头节点
private transient volatile Node head;
//未节点
private transient volatile Node tail;
// 当前持有锁的线程。 这个是aqs 的父类的属性。
private transient Thread exclusiveOwnerThread;
说下head 也就是队列的第一个节点 是一个虚拟节点 里面是没有线程信息的。他的下一个节点 才会使正常的排队的线程node。
为啥用虚拟节点? emmm… 其实不用这个虚拟节点也可以的。我觉得是为了屏蔽 真正节点的各种差异,,因为涉及到一些唤醒啊啥的操作 那种真正的节点 是否向后唤醒 会有好多判断啥的 判断起来特别麻烦。索性 抽出来也给head 节点 就好了。 个人感觉 大家也可以想想为啥哈哈
ps: 这里的变量都用到了volatile 修饰 为了保证可见性使用的。关于volatile 如果不了解可以去看下 我的这个文章嘻嘻小小支持一下
Node 结构
head 和 tail 的 类型是Node 这个Node 是 AQS 抽象类中的一个 内部类 他的结构就是一个 双向链表 。 这玩意东西挺多的眼花缭乱 刚开始看没啥用 反而直接劝退了。来瞅一眼精简版的Node 嘻嘻
static final class Node {
//节点 状态 (这个在 唤醒节点等时候有用 不急)
volatile int waitStatus;
//上一个节点
volatile Node prev;
//下一个节点
volatile Node next;
//阻塞的线程对象
volatile Thread thread;
}
线程挂起
LockSupport.park(Thread thread);
线程唤醒
LockSupport.unpark(Thread thread);
cas 替换
Unsafe 类 里面的方法
ok 相信大家看到这 不知道大家有没有这种想法 哎 差不多了 这aqs 也不过如此!!! 我觉得自己琢磨琢磨就能自己写个aqs了 哈哈哈 。 其实真正实现起来 里面细节可是太多了。哈哈 慢慢往下看 下面再将就得结合 具体的实现才能讲的清楚了 。
三,源码分析
1 ReentrantLock
手撕ReentrantLock 直接就是它 不解释。
ReentrantLock 它包含了 重入锁, 公平锁, 非公平锁 , 还有超时中断等功能。 这玩意一听就挺复杂啊··这是我能触及到的东西嘛。
慢慢来 先看下 asq 在reentrantLock 中是咋用的
囊 他没有 直接实现aqs 他也先整了个抽象类 Sync 去继承了 aqs
嗯 然后基于 公平非公平的 类型 去 整了两个Sync 的实现类
NonfairSync
FairSync
两个公平非公平的实现都有了 我怎么控制它啥时候用公平锁 啥时候用非公平锁啊 ok 看它的构造就知道了。
传true or false 想用哪个用哪个 啥都不传默认非公平。aqs 的state 代表持有锁线程的重入次数。 0 代表 没有锁持有 线程
好 行了 收 开始了。
lock() 方法
直接.lock() ctrl 点进去
emmm… 混子 不干实事。 具体执行在 sync 里面 sync .lock() 有两种不同的实现 一个是公平锁 一个是非公平锁的。 不慌 一个都跑不了。
我们可以先想一下 公平锁和非公平锁 的区别是啥 公平锁 是按顺序来的 先来后到 得先看下有没有人排队 如果没人排队才能枪锁。有人排队就不行 。 非公平锁 不管三七二十一 我先抢一手再说。
// 非公平锁
final void lock() {
//进来 我直接先抢一手。 就是通过cas 去更改 aqs 中的state 状态位改成1 这是说下状态0 代表 没有人持有锁
if (compareAndSetState(0, 1))
//如果我抢到锁 我直接设置 当前线程到 aqs 的 exclusiveOwnerThread 上面 代表当前持有锁的线程 (这个不用进队列 只有抢不到锁的 才会进入队列 抢到锁的不用进队列哦)
setExclusiveOwnerThread(Thread.currentThread());
else
//如果抢不到 执行这个方法。
acquire(1);
}
//公平锁
final void lock() {
acquire(1);
}
因为公平锁和非公平锁 都会 走这个 acquire 方法
非公平锁 先枪锁。 抢锁失败走这个方法。
公平锁 直接走这个方法。
这里思考一下 acquire 方法里面会干什么事 - -
公平锁应该会先判断下 state 状态吧 如果是0 看下队列前面有没有人排队。如果有人排队 加入到 队列里面。 如果没人排队
直接抢锁就好了。 非公平锁既然也会走这个方法。他是抢锁失败进入的 那他应该会直接去排队吧 哈哈。
ok 猜完了 直接看源码。
acquire
AbstractQueuedSynchronizer->acquire(int arg)
public final void acquire(int arg) {
// 我直接剧透
//再次获取锁如果获取失败 走下面
if (!tryAcquire(arg) &&
//先执行addWaiter构建Node节点放入队列 再执行acquireQueued for 死等 (挂起线程 等待执行)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//理论上 不会进入这个if 里面
//这个方法是设置线程中断标识位的 没啥用先忽略
selfInterrupt();
}
tryAcquire
AbstractQueuedSynchronizer-> tryAcquire(int arg)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
aqs 里这个方法直接抛异常了 ,意思是 如果要使用 acquire 那子类必须要实现这个方法咯
so ReentrantLock 里面实现了这个方法 。 公平锁和非公平锁 有不同的实现。
//公平锁
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取aqs 状态为 state
int c = getState();
// == 0 说明 没有线程持有锁
if (c == 0) {
//hasQueuedPredecessors 解读 这个代码块的最下面
if (!hasQueuedPredecessors() &&
//cas 抢锁
compareAndSetState(0, acquires)) {
//抢锁成功 把当前线程设置成持有锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
//如果上面判断失败 进入这里 看下是不是锁重入 如果是 state +1 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
return false;
}
//非公平锁 这个我就不说了大家自己猜下把哈哈哈
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
//========================================================================================================
//判断一下子 我前面是不是还有人 true 有人 false 没有
public final boolean hasQueuedPredecessors() {
//获取尾节点 和头结点
Node t = tail;
Node h = head;
Node s;
//先比较 头尾节点 如果相等。 说明 只有一个节点 并且是虚拟节点没有排队的 直接返回false
return h != t &&
// 头节点的下一个节点 不是 null 并且 是当前线程 返回false
((s = h.next) == null || s.thread != Thread.currentThread());
}
说白了 tryAcquire 方法就是抢锁的。 公平锁和非公平锁的区别就是 公平锁抢锁之前要看下是否是 排在队列的第一位通过hasQueuedPredecessors() 这个方法。
addWaiter
AbstractQueuedSynchronizer-> addWaiter(Node mode)
private Node addWaiter(Node mode) {
//以当前线程 构建 Node 节点
Node node = new Node(Thread.currentThread(), mode);
//获取aqs 中的尾节点
Node pred = tail;
if (pred != null) {
//把当前线程所属节点的 prev(上个节点) 指向 当前队列的最后一个节点
node.prev = pred;
// 然后 cas 替换 把 tail 指向 当前线程所属节点。
if (compareAndSetTail(pred, node)) {
//cas 成功 把 当前线程节点的 prev 的 next (下个节点 指向当前线程节点 )
pred.next = node;
//ok 了 说明加入队列成功了 直接返回当前node
return node;
}
}
// 如果说 tail == null (tail == null 说明 队列还没初始化完成) 或者是 cas 替换失败 。怎么办 猜一下。 没抢到锁 他是一定要加入队列的啊·· 所以他肯定是 要放进去的 咋放 死循环 哐哐硬放。
enq(node);
return node;
}
//===============================================================================
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//如果 tail 是null
if (t == null) {
//ok 了这里就是 这个队列的初始化操作了。 说虚拟节点就是在这里创建的。
//创建一个空节点通过cas 设置成 head (如果cas 失败说明有其他线程 也在初始化这个 队列 并且 设置head 的操作被别的线程抢到了 那我直接跳过 进行下一次循环就ok )
if (compareAndSetHead(new Node()))
//再设置 tail 节点为 head 节点。
tail = head;
} else {
// t 不是null 和addWaiter 时候一个操作 比较并替换 把现在的为节点 替换成 当前节点。
node.prev = t;
if (compareAndSetTail(t, node)) {
//cas 成功 把 当前线程节点的 prev 的 next (下个节点 指向当前线程节点 )
t.next = node;
return t;
}
}
}
}
大概意思:前面抢锁失败 构建Node 节点 塞到 queue 里面排队
如果上面tryAcquire 方法返回false 的话才会进入这个方法 至于参数 Node.EXCLUSIVE 不用管 这个是 用来标识 是共享锁还是排他锁的。我们这个都是排他锁
入队流程
1 先把当前 线程所属node 节点的 prev 设置成 当前队列的最后一个节点。
2 然后通过 cas 把 tail 替换成当前node 所属节点
3 最后 把 当前node 节点的 prev 节点的 next 属性设置成 当前node 节点。形成双向链表 大功告成。
思考 : 我们看源码的时候 相信大家也发现了 他只在设置 tail 节点的时候使用了 cas 操作 最后的 替换 prev 的next 的时候没有使用cas。 所以目前看 尾节点的插入只保证了 从后向前遍历链表的安全性, 并发情况下 从前向后遍历链表 是会有问题的(比如我cas 替换成功 说明我已经加入链表了, 但是还没有走到 第三步。 那 其他线程 去从head 向后 遍历 prev 的next节点就是null, 这个时候就丢节点了。 emm 这个问题具体体现再哪里 cas 又是怎么解决的 ?)
acquireQueued
ok 把 当前线程所属Node 加入到 队列中之后 就到了 acquireQueued 方法。目前我们还没看到线程挂起的操作在哪里 那肯定就是再 acquireQueued 里面了。
AbstractQueuedSynchronizer->acquireQueued(final Node node, int arg) ;
final boolean acquireQueued(final Node node, int arg) {
//这是初始化了一个 标记为 标识 是否失败。默认true
boolean failed = true;
try {
//线程中断标记为 默认false
boolean interrupted = false;
//又是一个死循环。 这个比较简单了 如果要走出这个死循环让线程继续执行 那真相只有一个 抢到锁 return 。
for (;;) {
//获取当前节点的上个节点 为啥不是 node.prev 这个 你点进去看下就知道了。。。不过是加了个校验而已。
final Node p = node.predecessor();
//如果你的上个节点是 头节点。 并且我通过 tryAcquire 拿到了锁
if (p == head && tryAcquire(arg)) {
//ok 那我终于可以执行了。 但是这个执行 就需要有些条件了 因为你要把 队列的节点 删掉呵呵。
// 变相删除 当前节点 再当前代码块的下面
setHead(node);
//方便gc具体可以自己去看下 jvm 垃圾回收算法哈哈 这里不过多赘述了
p.next = null;
//设置是否失败 为false
failed = false;
//返回线程中断标记位
return interrupted;
}
//如果没资格竞争锁资源 。 那就需要挂起了 这两个方法放在下面单独说下
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果线程中断标记为是 true 走这个 目前来说 不会出现这个情况的parkAndCheckInterrupt 方法 返回的一直是false (除非你自己去变更线程的中断标识- - )
if (failed)
cancelAcquire(node);
}
}
//================================================
private void setHead(Node node) {
// 把 head 指向当前node (不存在线程竞争问题了所以不需要cas)
head = node;
//把node 的 thread 设置 null
node.thread = null;
//node 的 prev 设置 null
node.prev = null;
//哈哈 其实就是通过把当前节点的整成 虚拟节点 把原来的虚拟节点去掉 变相的相当于删除 当前node 节点了
}
当抢锁线程 掉用 lock 方法 如果没有抢到 就会再这里挂起线程 等待唤醒 ,唤醒之后继续抢锁。抢不到继续挂起 知道抢到锁 才会继续执行lock 之后的代码。
shouldParkAfterFailedAcquire
AbstractQueuedSynchronizer->shouldParkAfterFailedAcquire(Node pred, Node node)
这个方法很重要 我们之前说的node 节点的状态位 waitStatus 变化 目前来说 都集中再这里!! 这里回顾下 我们目前 了解到的节点 的初始 状态位。
1 头节点(虚拟节点)
head 在队列被创建的时候初始化的 它直接new Node() 出来的 构造里面也没有对 waitStatus 做任何操作 所以 它默认值是0
在 排队的线程拿到锁的时候也会被初始化 通过那个setHead 方法嘛 那个方法 里面头节点 用的是 拿到锁的线程节点的 waitStatus 值
2 普通节点
目前的普通节点的创建是 在addWaiter 里 通过 Node node = new
Node(Thread.currentThread(), mode); 创建的 默认值也是0
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取当前节点 的上个节点的 状态位
int ws = pred.waitStatus;
//如果是 -1 直接return ture 回去。
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
//如果大于0 目前来说昂 是没有大于0 的情况的
// 在这个逻辑里 看样子 大于0 的节点 属于是被取消的节点。 会被直接从队列删除掉 一个循环 从后向前 把找到的第一个 不大于0 的节点作为 当前线程节点的上个节点。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 那就是 不是 -1 并且 <=0 的情况了
//这种情况 就通过cas 直接改为 -1 就ok
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
注意点
1 这个方法 对状态的变更 只针对 当前节点的上一个节点
2 只有当前节点的上一个节点 的waitStatus 是 -1 才会返回true
3 如果上个节点状态不对(>0) 会把上个节点从队列删除 并重新设置当前节点的prev
so node节点 的waitStatus = -1 代表它的下个节点是 可休眠状态。
总结
这个方法说白了 清空 当前节点到上个可用节点之间的废弃/取消节点, 并更新 当前节点上个节点的waitStatus 为 -1。
parkAndCheckInterrupt
AbstractQueuedSynchronizer->parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
如果上面的 shouldParkAfterFailedAcquire 方法 返回 true 则执行线程挂起。
unlock() 方法
不管是公平锁还是非公平锁 释放锁 都用的同一个方法
public void unlock() {
sync.release(1);
}
// aqs 里面的 release
public final boolean release(int arg) {
//判断 锁是否释放干净 可重入锁 加锁次数要和 释放次数一致才算真正释放锁
if (tryRelease(arg)) {
//如果释放干净 那就得准备唤醒下一个节点了
//获取头节点
Node h = head;
// 目前情况 头节点不可能是null (我到现在都搞不到 啥情况 走到这一步的时候头节点回事null)
//并且头节点的 状态 不是0 0 其实就代表 节点后没有需要唤醒的线程。 为啥 如果你后面真有节点 那在 shouldParkAfterFailedAcquire 方法里 就会把你的状态变为 -1 所以要么你是0 后面没有节点 要么是-1 后面有节点(目前情况来看)
if (h != null && h.waitStatus != 0)
//如果后续有节点 唤醒后续节点
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease
ReentrantLock-.Sync->tryRelease
protected final boolean tryRelease(int releases) {
//获取 aqs 的state - 1
int c = getState() - releases;
//判断下 释放锁的线程是否是持有锁的线程 如果不是抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果 state - 1 后是0 说明释放干净了
if (c == 0) {
//设置返回值
free = true;
//设置持有锁的线程 为null
setExclusiveOwnerThread(null);
}
//把 -- 后的state 设置回去
setState(c);
return free;
}
unparkSuccessor
AbstractQueuedSynchronizer->unparkSuccessor(Node node)
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//如果头节点状态<0 把他设置成0
// emm 这个操作 我觉得是为了防止重复唤醒。
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//获取头节点的下一个节点。
Node s = node.next;
//如果下一个节点为null || 下个节点节点状态 > 0 也就是取消状态
if (s == null || s.waitStatus > 0) {
//从后向前遍历 拿到 最靠近head 的节点
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//ok 唤醒。
if (s != null)
LockSupport.unpark(s.thread);
}
ps: OK OK 先到这里吧= = 困了 其实还有tryLock 和 带参数的tryLock 等方法 还有读写锁的那种。 我都还没说。 因为写博客真实太麻烦了··哈哈哈哈so 下次一定哈哈 后续我会更新的