实在想不到好的标题了,凑合着用吧...网上关于
AQS的内容也很多,但感觉不是很清晰,所以抽空自己写了一篇,说是秘密花园,其实也就那么一点,相对来说会细一点,有不同看法的可以在下方评论哦~
1. ReentrantLock
关于ReentrantLock的使用之类的,大家应该都很清楚,这里就不阐述了。
ReentrantLock就是AQS独占式实现的同步组件中的一员,这里就以ReentrantLock(非公平锁)。为例,解开AQS的神秘面纱
同步组件的主要实现方式是继承,子类继承同步器,并实现抽象方法,来管理同步状态,子类推荐被定义为自定义同步组件的静态内部类,ReentrantLock就是使用的这样的形式:
下面两个就是ReentrantLock的公平实现和非公平实现,默认非公平实现,这里就以非公平实现来理解ReentrantLock如何与AQS进行关联的:加锁源码如下:
final void lock() {
// 通过CAS设置State变量的同步状态 0->可同步状态 1->以同步状态
if (compareAndSetState(0, 1))
// 设置独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
设置成功就设置为独占线程。吐槽:其实在设置同步状态这里一直有个问题一直想不通,就是两个线程都进去了compareAndSetState()该方法,其中一个线程先成功设置了,那问题来了,另一个线程是会一直重试还是返回呢?我会有这个问题就是因为在看CAS时候,大部分帖子说CAS失败会自旋,于是我便陷入了CAS自旋,导致见到CAS就自旋,在加上再写这文章时查阅的资料,没有获取到锁的线程会自旋啥的,以致于对后续没有获取到同步状态的线程是入队和还是自旋发生了冲突: 既然失败,按照"道理"应该自旋等别获取到同步状态,那为啥还要队列呢?其实也很简单,网上关于CAS都是使用AtomicInteger来操作的,他的源码是这样的:
而ReentrantLock这里是这样的:
AtomicInteger会自旋没问题,因为他是do while而ReentrantLock是不会自旋,那网上说的自旋是在哪呢?后面会说到。
如果设置失败呢?后续如何处理?是直接跳过?还是等待?那就要看acquire()方法的实现了,该方法在ReentrantLock中没有实现,而是在他的父类,也就是今天的主角:AbstractQueuedSynchronizer
2.AQS
AbstractQueuedSynchronizer简称AQS,翻译为队列同步器,用来构建锁或其他同步组件的基础框架,自身没有实现同步组件,对外提供模板方法共自定义组件使用。AQS既支持独占式的获取同步状态,也支持共享式的获取同步状态,以方便实现不同的锁或同步组件,可以说对锁或同步组件的实现者非常友好。
AQS控制同步状态的核心思想:如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。
这里面涉及到的一些属性可能没有讲到,可以自行了解,提升下源码阅读能力(就是我懒).
锁分配机制
在AQS内部维护着一个 FIFO双向队列,无法获取到同步状态的线程将被构成Node节点,并放入同步队列尾部,同时会阻塞当前线程,当同步状态被释放时,会唤醒首节点中的线程去获取同步资源,下面是同步队列的基本结构:
线程入队
假设模拟三个线程,其中一个获取到同步状态后,并设置独占,那后续获取失败的线程如何入队?这正式我们接下来要说的:
// 获取同步状态失败后执行的方法 arg为1 就是为了便于后续进行同步操作
public final void acquire(int arg) {
// tryAcquire:入队前挣扎一下,看看拿到锁的那个哥们有没有释放,试着获取锁
// 为了方便,我就把方法贴下面了,执行完该方法后,发现两哥们运气差,第一个哥们还没执行完
// 拿那没办法了,使用addwaiter()开始入队,分一下段。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 中断当前线程
selfInterrupt();
}
// tryAcquire()最终会调用该方法,你要记住此时有两个哥们在出发
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取线程同步状态,state是volatile修饰int类型的变量,该方法只是返回了该变量
int c = getState();
// 0表示没有占着锁,两个哥们可以开抢了
if (c == 0) {
// 该操作是原子性的,这里使用的就是CAS 0,表示期望值,acquires为1,表示替换的值
// Java中的CAS操作利用了处理器提供的CMPXCHG指令实现,
// 通过该指令操作的内存区域就会加锁(还是得看处理器,单处理器自身会维护),
// 导致其他处理器不能同时访问它
// 指令挺多的,可以查阅:Java并发编程艺术
// 上面一段也就是说只有一个哥们能获得同步状态
if (compareAndSetState(0, acquires)) {
// 设置线程独占
setExclusiveOwnerThread(current);
// 返回true 获取同步状态完成
return true;
}
}
// 假设两哥们都没成功,那就判断是否属于再次获取锁(重入锁)
else if (current == getExclusiveOwnerThread()) {
// 状态值加1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置state的值,注意:volatile对于 i = 1这类操作具有原子性
setState(nextc);
// 返回true 获取同步状态完成
return true;
}
return false;
}
线程获取同步状态失败后会执行addWaiter()方法来将该线程加入队列:
// 两个哥们都进来了 mode->该参数的值为null
private Node addWaiter(Node mode) {
// 构建节点 两个线程都构建
Node node = new Node(Thread.currentThread(), mode);
// 获取尾节点
// 第一次 两个哥们都是空的,进入下方的enq,各自把自己的节点带进去了
Node pred = tail;
// 是快速尝试添加尾部节点,避免进入enq的死循环,分担一点压力
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 第一次两个哥们都进来了
enq(node);
return node;
}
private Node enq(final Node node) {
// 死循环决定谁在尾部节点
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
/**
* 这是这个方法的源码,只有为null 才会构建一个空节点肯,定要一个哥们去构建空节点
* private final boolean compareAndSetHead(Node update) {
* return unsafe.compareAndSwapObject(this, headOffset, null, update);
* }
* 1. 此时第一遍执行完了 AQS队列处于图1.1状态,还没有返回,继续下一个循环
*/
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 第二遍的时候进来了,此时的node是我们线程各自带进来的,prer代表这个节点的前驱节点
// 两个哥们的前驱节点都指向刚才初始化的空节点,效果如图1.2,但是并不是真正的指向
// private final boolean compareAndSetTail(Node expect, Node update) {
// return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
//}
// 这个时候和上面一样只有一个哥们可以成功,成功的这个将会添加到尾部
// 另一个将孤独的在循环一遍,同样也是添加到队尾,该方法执行完效果如图1.3
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
图1.1:
主要流程:
-
通过当前线程和锁模式构造出一个节点.
-
快速尝试在尾部添加节点,在尾部节点不为空的情况下,通过
CAS将新建的节点添加到尾部。CAS失败将进入enq()方法,简单理解:博一博,单车变摩托。减少进入enq()死循环的必要。 -
如果上面博失败了,或者根本没有尾节点,都将进入
enq()方法,如果成功则将新建的节点添加到队列尾部,如上图1.3。
总结一下上面的流程(有些可能没讲到...):
addWaiter()只是将没有获取到的线程包装成一个节点放入AQS的队列中,同时返回该节点,接下来就是acquireQueued()的时间了:讲道理,一看望去,我也头疼...
// node: 刚才创建的节点 arg:1 对排队中的线程进行“获锁”操作
final boolean acquireQueued(final Node node, int arg) {
// 是否成功拿到资源
boolean failed = true;
try {
// 是否中断过
boolean interrupted = false;
// 自旋获取锁或者中断,到这里才是自旋获得锁😭
for (;;) {
// 获取当前节点的前驱节点,这里假设两个哥们都进来了
// 参照上图1.3 节点二的前驱节点是节点一,节点一的前驱节点是虚节点
final Node p = node.predecessor();
// 如果前驱节点是头节点 说明在队列头部 就 尝试获取同步状态 对照上一步流程图
// 第一个哥们就尝试去获取锁, 注意: 头节点是虚节点,真实数据首节点是节点一
if (p == head && tryAcquire(arg)) {
// 这个是第一个哥们的做法,获取到锁了
// 成功执行以下方法,总的来说就是如果第一个哥们运气好,他就变成虚节点了,
// 把队列的head指向当前节点,当前节点的线程变成空,prev变成空
// 如图1.4
setHead(node);
// 原理的虚节点可以回收了
p.next = null; // help GC
failed = false;
// 该线程不用被中断
return interrupted;
}
// 头节点没有获取到锁,或者不是头节点,两哥们又在一起了,这个时候就要判断当前node要不要被阻塞
// 防止一直自旋带来的消耗,不加这个的话,上面执行完,又开始重新循环了,直到获取到资源
// 第一轮结束 shouldParkAfterFailedAcquire(p, node)返回 false
// 在执行一遍上面的循环 假设依旧和上面的情况一样,又进入了该方法
// 第二次shouldParkAfterFailedAcquire(p, node) 返回true,执行parkAndCheckInterrupt()
// 谁执行parkAndCheckInterrupt();就会阻塞,不会继续往下执行,很悲剧两个哥们都停住了
// 第二次执行往 队列状态如图1.5,加锁到这就结束了,这是最普通的一个加锁方式,会一直阻塞到解锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire():
// pred->当前节点的前驱节点,node->当前节点
// 第一个哥们的前驱节点为虚节点,第二个哥们的前驱节点为第一个哥们
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//第一次: 第一个哥们 ws = 0,节点默认为0 此时还没做更改,第二个哥们 ws = 0,
// 第二次: 第一个哥们 ws = -1 ,第二个哥们 ws = -1,
int ws = pred.waitStatus;
// Node.SIGNAL = -1 第一次:0 == -1 = false
// 如果虚节点的状态等于 -1 头节点处于可唤醒状态
// 第一次 两个哥们都没有进入
// 第二次 进来,两个哥们都是true,直接返回了
if (ws == Node.SIGNAL)
return true;
// ws 大于0表示线程获取锁的请求取消了
// 第一次进来依旧false
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 第一遍的时候两个哥们都进来了
// private static final boolean compareAndSetWaitStatus(Node node,int expect,int update) // {
// return unsafe.compareAndSwapInt(node, waitStatusOffset,expect, update);
//}
//
// 设置前驱节点的waitStatus = -1,第一遍时结束 虚节点和第一个哥们的waitStatus = -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
图1.4
图1.5
总结下加锁流程:
加锁完了,自然是解锁咯:
// 解锁方法,从tryRelease方法开始
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 假设没有重入,返回true
if (tryRelease(arg)) {
// 获取头节点,注意是虚节点, 状态为-1
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒虚节点的下一个节点,同时虚节点状态变为0,到这里流程就结束了
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// 重入锁的时候会+1,这时候就要解锁...
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;
}
ReentrantLock的普通加锁方式还是比较好理解的,还有一些其他的各位自行了解(😁),如哪里编写错误,欢迎指正~