来源于近期的一次分享,介绍下并发中AQS的实现与使用。
1、AQS组成
-
violate int state的变量,来表示同步状态,通过CAS完成对State值的修改
-
FIFO双端队列(是CLH变体的虚拟双向队列),来完成资源获取的排队
1.1 state介绍
state的值大于1体现可重入性。
state的值来判断是否加锁(以ReentrantLock为例,state为0,没有锁,大于0,视为加锁)
| 描述 | |
|---|---|
| protected final int getState() | 获取State的值 |
| protected final void setState(int newState) | 设置State的值 |
| protected final boolean compareAndSetState(int expect, int update) | 使用CAS方式更新State |
1.2 FIFO双端队列
Node节点组成的双向链表
示意图:
在AQS中,没有获取锁的线程,打包到Node节点中。
2 案例分析
这里只是描述了简单的情况,后续会对此案例进行剖析,thread0在获取锁时有可能多次获取,此时state的值是大于1,逐渐修改为0
多个线程共同访问共享资源的情况下,是如何维护同步状态state以及同步队列(FIFO队列)
步骤分析:
- thread 0 获取资源,此时state = 0,此时thread0 修改状态state = 1;thread1加入同步队列时,还会通过CAS尝试获取锁
-
此时thread 1 尝试通过CAS方式尝试获取锁,因为锁已经被thread0 持有,此时thread1 进入FIFO同步队列;同样的,thread 2、thread 3、thread 4 加入同步队列
-
假设thread0需要释放锁,首先会将state的值由1改为0,thread1被唤醒,通过CAS方式获取锁,当thread0获取锁的时候,FIFO会使队头元素出队,
3、从源码中剖析案例问题
3.1、获取同步状态State的两种模式
独占方式和共享方式:指获取state的方式;一个线程使用独占方式获取了资源,其它线程就会在获取失败后被阻塞。一个线程使用共享方式获取了资源,另外一个线程还可以通过CAS的方式进行获取。如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配,AQS 中会将竞争共享资源失败的线程添加到一个变体的 CLH 队列中(公平/非公平)。
3.2、非公平锁体现在哪里(出队)
上述可知,非公平性是指线程不是按照FIFO的方式获取锁,下面从入队和出队来分析非公平性这个过程。
(1) 入队时:在thread1通过CAS获取锁失败后,会加入FIFO同步队列中,加入队列末尾。
线程在获取锁失败时,通过addWaiter()方法被加入到等待队列;
如果无法通过快速路径将线程插入队列尾部,enq()方法会负责将节点放入队列末尾。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速将节点插入到队列尾部
Node pred = tail;
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) { // 必要时初始化队列
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在addWaiter()方法中:
- 线程首先被包装成一个
Node对象。 - 然后尝试将这个新节点通过CAS操作插入到队列的尾部。
如果初次尝试插入尾部失败(例如,由于竞争导致tail节点已更新),则调用enq()方法,正式进入等待队列。
enq()方法通过一个自旋循环,确保新节点最终被插入到队列的末尾。- 如果队列还未初始化(即
tail == null),它会首先初始化队列。 - 随后,它通过CAS操作不断尝试将当前线程节点插入到队列的末尾,直到成功。
(2)出队时:当thread0锁释放后,此时threa1被唤醒,FIFO通常只唤醒队头元素,但此时如果thread5来获取锁,此时thread1和thread5来竞争,此处体现非公平性。
虽然唤醒了队列头部的线程,但新来的线程依然可以在队列头部线程获取锁之前直接竞争锁。这意味着即使队列头部的线程被唤醒,它也不一定能马上获取锁,而新来的线程有机会“插队”获取锁。