AQS源码一窥-JUC系列
AQS源码一窥
考虑到AQS的代码量较大,涉及信息量也较多,计划是先使用较常用的ReentrantLock使用代码对AQS源码进行一个分析,一窥内部实现,然后再全面分析完AQS,最后把以它为基础的同步器都解析一遍。
暂且可以理解AQS的核心是两部分组成:
- volatile修饰的int字段state,表示同步器状态
- FIFO同步队列,队列是由Node组成
节点模式
Node定义中包含的字段,意味着节点拥有模式的属性。
独占模式(EXCLUSIVE)
当一个线程获取后,其他线程尝试获取都会失败
共享模式(SHARED)
多个线程并发获取的时候,可能都可以成功
Node
中有一个nextWaiter
字段,看名字并不像,其实这个是两个队列放入共用字段,一个用处是条件队列下一个节点的指向,另一个可以表示同步队列节点的模式,可以在下面代码的SHARED和EXCLUSIVE定义中看到。
因为只有在独占模式下才会有条件队列,所以只需定义一个共享模式的节点,就可以区分两个模式了:
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/**
* Link to next node waiting on condition, or the special
* value SHARED. Because condition queues are accessed only
* when holding in exclusive mode, we just need a simple
* linked queue to hold nodes while they are waiting on
* conditions. They are then transferred to the queue to
* re-acquire. And because conditions can only be exclusive,
* we save a field by using special value to indicate shared
* mode.
*/
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
SHARED
是静态变量,地址不会变更,所以直接使用isShared()
方法直接判断模式。独占模式就像普遍认知的锁能力一样,比如ReentrantLock
。而共享模式支撑了更多作为同步器的其他需求的能力,比如Semaphore
。
节点状态
节点状态是volatile
修饰的int字段waitStatus
。
- CANCELLED(1):表示当前节点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- 0:新节点入队时的默认状态。
正数表示节点不需要唤醒,所以在一些情况下只需要判断数值的正负值即可。
AQS独占模式源码
从ReentrantLock
入手了解一下AQS独占模式下的源代码。
测试代码:
public class AQSTest implements Runnable{
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
AQSTest aqsTest = new AQSTest();
Thread t1 = new Thread(aqsTest);
t1.start();
Thread t2 = new Thread(aqsTest);
t2.start();
t1.join();
t2.join();
}
/**
* 执行消耗5秒
*/
@Override
public void run() {
reentrantLock.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
测试代码模拟了两个线程争抢锁的场景,一个线程先获取到锁,另一个线程进入队列等待,5秒后第一个线程释放线程,第二个线程获取到锁。
获取锁
AQS
中的acquire
方法提供独占模式的获取锁能力。
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
@ReservedStackAccess
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
先执行tryAcquire
成功就结束,失败就进行入队等待操作。
入队
addWaiter
根据传入的mode为当前线程创建一个入队的Node。这里有一个前提就是执行入队流程意味着已经发生竞争的情况,这一个前提可以帮助到读下面的代码。
/**
* 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) {
// 创建Node
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 先执行一次快速路径入队逻辑(在竞争前提下,预判头尾节点都已经初始化好了)【1】
Node pred = tail;
// 尾节点不为空
if (pred != null) {
node.prev = pred;
// 尝试在尾节点后面入队 【2】
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 (;;) {
Node t = tail;
// 这个尾节点为空表示未初始化头尾节点过 【3】
if (t == null) { // Must initialize
// cas设置头节点【4】
if (compareAndSetHead(new Node()))
// 尾节点和头节点保持一致
tail = head;
} else {// 这个分支和前面的快速路径入队逻辑一致【5】
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* CAS head field. Used only by enq.
*/
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
/**
* CAS tail field. Used only by enq.
*/
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
【1】,快速路径入队,这个
fast path
的思路在本类其他代码中也有,我的理解是在代码分支上预判某个分支是大多数情况发生的分支,所以优先执行,如果不是没有进入再走完整兜底代码。这里enq
就是完整兜底代码,其中有处理头尾节点初始化逻辑,因为头尾节点是队列生命周期中只执行一次的操作,大部分场景是不需要考虑初始化头尾节点的分支,所以才有了这里所谓的fast path
【2】,cas操作尾节点成功,才执行尾部入队的最后一步操作:原尾节点的next指向自己。对于一个双向链表,在尾部插入一个元素需要两步:A,自己的prev指向当前的尾节点;B,当前尾节点的next指向自己。而在AQS的同步队列里还有一个tail指向当前尾节点,所以又多了一步就是需要把tail指向自己,一共三步。回过头再仔细阅读下代码,它的操作步骤是,先设置自己prev指向可能的尾节点,然后cas操作tail(
compareAndSetTail
)指向到自己,如果成功,就更新尾节点的next指向自己。在并发场景中,cas是可能失败的,所以自己的prev可能需要不断地变更,而当前队列中的尾节点的next是在cas设置tail后才操作,只变更一次。【3】,头尾节点都是延迟初始化(lazily initialized),在没有需要入队操作前都不会进行初始化。初始化就是new出一个
waitstatus
为0的Node设置给head,然后尾节点赋值(tail = head;
)。【4】【5】,初始化头尾节点由两步操作组成,头节点cas设置成功后,才会设置尾节点,所以可以确定只要尾节点不为null,头节点就一定不为空。
假设
compareAndSetHead
成功设置head后,执行尾节点赋值时尾节点会不会已经被其他线程修改了呢?不会,因为
compareAndSetHead
操作只在enq
方法调用,也只有在头节点未初始化时触发,而如果初始化头节点成功后,此时的tail还一定是null,所以前面的逻辑里都进不了操修改tail不为null的分支代码,只能进入初始化头尾节点的分支,所以会在compareAndSetHead
上自旋,直到tail设置结束,就可以进入tail不为null的分支代码了。再仔细想一下这个设计只要先判断的是tail是否为空就相当于判断了初始化是否结束。
下图是这种场景同步队列节点变化情况:
- 1,初始时同步队列的head和tail都为null,state是0
- 2,当第一个线程获得锁,就会把state置成1,此时head和tail都为null,因为还没出现竞争情况,没有必要初始化头尾节点。而当再有线程来获取锁的时候就需要进行入队等待了,
enq
方法中自旋的第一次循环会触发初始化头尾节点,这个节点的thread是null,waitStatus是初始化状态0,next和prev的指向也都是null。 - 3,初始化好头尾节点后,接下去就是把新创建的Node放到同步队列的尾部。
acquireQueued
前面已经在队列里入队成功,然而线程还没进入等待状态,接下去自然是把线程转成等待了,就像物理上已经处理好入队了,还差法术上的入队等待了。
/**
* 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
*/
@ReservedStackAccess
final boolean acquireQueued(final Node node, int arg) {
// 标识是否获取失败
boolean failed = true;
try {
// 标识线程是否中断(等待是在下面的自旋中,将来唤醒后会检查线程中断状态)
boolean interrupted = false;
// 自旋【1】
for (;;) {
// 获得当前节点的前节点
final Node p = node.predecessor();
// 如果前面已经是头节点了,那么代表机会来了,进行一次tryAcquire,尝试获取锁【2】
if (p == head && tryAcquire(arg)) {
// 更新头节点
setHead(node);
// 断开前节点next引用
p.next = null; // help GC
failed = false;
return interrupted;
}
// 检查是否需要park 需要的话就进行线程等待【3】
// shouldParkAfterFailedAcquire这个方法逻辑就是我要躺平休息了得确定前面有人能叫醒我
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 取消尝试获取锁的节点
cancelAcquire(node);
}
}
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前节点的waitStatus
int ws = pred.waitStatus;
// 已经是SIGNAL状态,就直接返回
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 如果是大于0的状态表示前面节点是取消状态,但是还没有从队列上移除
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
// 这里移除状态大于0的节点,就是把当前节点的prev往前移
node.prev = pred = pred.prev;
// 前移直到找到一个不是取消状态的节点
} while (pred.waitStatus > 0);
// 前节点next设置(双向链表常规操作)
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设置前节点状态为SIGNAL,这个cas操作需要外面的调用方再一次确认是否真的不能获取锁后再进行park操作
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* CAS waitStatus field of a node.
*/
private static final boolean compareAndSetWaitStatus(Node node,
int expect,
int update) {
return unsafe.compareAndSwapInt(node, waitStatusOffset,
expect, update);
}
/**
* Sets head of queue to be node, thus dequeuing. Called only by
* acquire methods. Also nulls out unused fields for sake of GC
* and to suppress unnecessary signals and traversals.
*
* @param node the node
*/
// 将传入的节点设置为head,并且抹去节点中不必要的引用,注意没有cas操作,
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
【1】这个自旋里,有修改前节点状态失败或者前节点有取消的状态情况而需要自旋。
【2】如果前节点已经是head,那么意味着自己有资格争夺锁资源,当然如果没有获取到,那还是乖乖走等待的逻辑,如果获取到,表示此前面节点入队的时候没有获取到锁,而此时锁已经释放,那么自己就会成为获得锁的线程,队列中自己节点就会替换当前头节点成为新的head。
方法
setHead
没有做自旋操作,是简单几个赋值操作集合,因为这个方法是确保tryAcquire
或tryAcquireShared
成功后执行的,所以不需要考虑并发情况。方法中会把thread和prev都置空,其实获取到锁的节点内这两个信息已经没什么作用,并且自己的前节点的next也会值空,切断对自己引用。【3】同步队列中是看自己Node里的waitStatus是什么来决定是否唤醒后节点,如果是
SIGNAL
状态,就会唤醒后节点。所以每个排队的节点在自己进入等待状态前都需要确保前节点的状态是SIGNAL
状态,这样就可以保证未来是可以被唤醒的。这就是shouldParkAfterFailedAcquire
方法做的事。shouldParkAfterFailedAcquire
方法名也明确表达了这个是线程park的前置条件判断,只要这个方法返回true,线程就可以安心去等待了。具体方法实现代码我们详细再继续往下看会发现,只有判断出前节点waitStatus是SIGNAL
状态才会返回true,其他还有两种情况:A,前节点状态为取消状态,就会进行前节点引用前移,直到前节点不是取消节点,然后退出方法继续自旋;B,前节点是0或者PROPAGATE
状态,就进行cas修改为SIGNAL
状态,无论成功或失败都是退出继续自旋。所以前节点除了已经是SIGNAL
状态,其他情况都会再进行自旋,自旋的开始就会进行一次头节点的判断,以保证本次自旋在head后节点能够快速进行一次获取操作。上面【2】中提过,在没有获取到的情况下还是会走等待的逻辑,那么也就是说head节点的waitstatus状态必须已经是SIGNAL
状态了。
延续前面的测试代码,继续图解节点数据的变化:
补充说明:
因为测试代码是一个线程获取锁,一个线程等待,所以队列中只会有两个节点一个head,一个等待节点,在等待节点设置前节点waitStatus的自旋代码中对前节点是否为head的判断就为true,所以在第一次自旋的时候会执行一次tryAcquire
,然后执行shouldParkAfterFailedAcquire
后将head节点的waitStatus更新为SIGNAL
状态后再会自旋执行一次tryAcquire
,因为前节点还未释放锁,所以两次tryAcquire
都失败,然后才执行park,线程进入等待状态。
acquireQueued
方法中的最后finally代码块中,判断failed字段是否为true,如果是就会执行cancelAcquire
方法取消节点,那么什么时候会发生failed为true的情况呢?已经有同学也思考过这个问题。我还有一个理解是:本来AQS就是以一个框架形式提供,子类实现一些方法达成自己想要的同步器形式,这里的tryAcquire
方法就是子类实现的,既然是子类扩展实现的那就没法保证这个方法是否会跑出遗产中断自旋而导致执行到cancelAcquire
方法。
顺便也读下cancelAcquire
方法的源码:
/**
* Cancels an ongoing attempt to acquire.
*
* @param node the node
*/
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
// 这段遇到取消状态节点就把节点前移代码和shouldParkAfterFailedAcquire一致
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
// 如果自己是尾节点,操作就比较简单,cas操作tail指向,然后把前节点的prev指向设置成null就结束了
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
// 不是tail,也不是head的后节点,判断waitStatus是不是SIGNAL,如果不是就cas设置一次为SIGNAL
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 {
// 前节点是head,此时自己的waitStatus是CANCELLED,unparkSuccessor会跳过自己节点去唤醒自己后符合条件的节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
释放锁
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
@ReservedStackAccess
public final boolean release(int arg) {
// 首先就进行一次释放操作【1】
if (tryRelease(arg)) {
// 持有锁的节点永远是头节点【2】
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒后节点线程
unparkSuccessor(h);
return true;
}
return false;
}
- 【1】这个释放操作是先执行的,只有成功才会进入从头节点往后唤醒后节点的操作,所以在后续
unparkSuccessor
的代码逻辑中是有这个重要前提条件的,需要特别注意。 - 【2】这里head是不可能为null的,这个是由整个同步队列机制决定的,无论是初始化的头节点还是后面将看到的被唤醒获得锁的节点替换成为头节点,可以认为头节点表示着获取锁的节点,虽然这个头节点是不维护线程。然后会判断head的waitStatus状态不为0,因为前面入队代码中已经提过在把自己线程park前会需要先把前节点设置成
SIGNAL
状态。
假设测试代码中的unlock执行,节点数据的变化如下图:
唤醒后节点线程
unparkSuccessor
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
// 只要状态是小于0,就进行一次cas设置为0【1】
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
// 此时没有后节点或者后节点状态是取消状态【2】
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)
// 唤醒节点持有线程【3】
LockSupport.unpark(s.thread);
}
- 【1】执行一次cas重置waitStatus,不过没有自旋加持,所以是允许失败的
- 【2】在注释中信息是这样:需要唤醒的线程在下一个节点上,如果从next指向的节点不符合唤醒的节点(null或状态为取消),那么就从队列尾部开始往前找那个没有取消的节点,当然也有可能没找到需要唤醒的节点。注释没有说明为什么需要这么做,我们再回顾下放入队列尾部节点的代码分析(入队【2】),
compareAndSetTail
成功保证了当前节点的prev和队列的tail的指向是成功的,而最后一步pred.next指向是在cas操作成功后执行的,会有这样的场景就是cas执行成功还没执行到pred.next指向操作,那么此时队列从前往后找一个没有取消的节点会找到的是null,而从尾往前遍历就没有问题。 - 【3】
unpark
操作对应的前面入队等待park
操作,也就是说唤醒的线程会从那时等待的地方继续往下执行。继续执行的代码就是acquireQueued
中自旋的部分。所以当唤醒等待的线程后自旋代码就会检查自己节点的前面是不是head,如果是就会进行一次获取锁操作,如果不是就执行shouldParkAfterFailedAcquire
方法。
按前面例子里的unlock
触发释放锁,先执行unparkSuccessor
方法更新头节点的waitStatus为0,然后会unpark
后节点线程,被唤醒的线程开始执行acquireQueued
方法的自旋,判断当前线程节点的前节点就是head,那么就会执行tryAcquire
返回成功,然后开始替换头节点。
队列节点数据变化如图:
ReentrantLock源码
ReentrantLock基于AQS实现的可重入锁,支持公平和非公平。
进行了前面AQS代码的解析,ReentrantLock的代码变得异常简单,考虑到篇幅有限,下面只对公平性和可重入性进行解析,在后续文章中的还会再使用ReentrantLock。
公平/非公平
ReentrantLock内部实现了一个内部抽象类Sync
,它的子类有FairSync
和NonfairSync
,看名字就明白了具体公平和非公平就是这两个类的实现不同了。
AQS内置的FIFO同步队列,入队后天然是公平的,什么时候会出现不公平的情况呢?
在这里的不公平是指:一个刚来获取资源的线程会和已经在队列中排队的线程产生竞争,队列里等待的线程运气不好一点始终竞争不过新来的线程,而新来的线程假如源源不断过来,队列里等待的线程获取成功等待的时间就很长,那么就会出现所谓的线程饥饿问题,这个就是这里需要解决的不公平。而公平就是按入队的顺序来决定获取资源的顺序,那么这个新来的线程就应该在所有已经入队的线程之后再来获取。要达到这个公平的效果就是每个线程进来获取的时候,先判断一下是否有其他线程已经在等待获取资源了,如果有就不用去获取了,直接去入队就行了。
hasQueuedPredecessors
判断的方法就是hasQueuedPredecessors
:
/**
* Queries whether any threads have been waiting to acquire longer
* than the current thread.
**/
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
截取部分方法注释:只要有线程等待的时间比当前线程长就应该返回true,否则返回false。
虽然这个方法的判断代码不多,可是直接看会有点懵,但是有了前面的代码解析铺垫,这个代码瞬间看懂。
一个关键的关联信息是前面介绍的enq
方法中初始化头尾节点,我们已经知道初始化头尾节点不是原子操作,分成两步操作:
compareAndSetHead(new Node()) // 1
tail = head // 2
所以就从初始化头尾节点的角度来分析下这个判断,以下是三种场景下的情况的解析:
- 1,完全未初始化,也就是没有出现过竞争场景,所有head和tail都是null,
h != t
为false,返回false - 2,初始化到一半,也就是执行完
compareAndSetHead(new Node())
还没执行tail = head;
,tail为null,head不等于null,h != t
为true,因为此时head的next指向还是为空的,(s = h.next) == null
为true,返回true。这个场景意味着有线程因竞争而触发初始化头尾节点,虽然还没有进行入队成功,但还是认为它是先于当前线程的。 - 3,初始化结束,对于
h != t
有几种情况:- 队列中只有head节点,也没有获取成功的线程,因为已经初始化结束,所以head和tail指向同一个对象,
h != t
为false,返回false - 队列中只有head节点,有获取成功的线程,因为已经初始化结束,所以head和tail指向同一个对象,
h != t
为false,返回false,看起来和第一种情况相同,展开还有以下情况- 没有线程在入队,这种情况就是只有当前这个线程在获取操作,所以不需要排队,返回false没问题
- 有线程正在入队,只是
compareAndSetTail
操作还未成功,这种场景也可以是不考虑的,因为对于入队的先后顺序是cas操作,代码在cas未成功前并不确定哪个线程的先后情况。
- 队列中除head节点还有1个节点情况,这个情况就是head后的节点入队成功,表示保证了
compareAndSetTail
操作成功,h != t
为true,那么也有两种场景,- 已经执行过head的next指向操作(
t.next = node
),(s = h.next) == null
是false,返回的结果就是s.thread != Thread.currentThread()
的结果(如果第二个节点是自己返回true,如果不是返回false) - 还没有执行head的next指向操作,
(s = h.next) == null
是true,返回true,这个场景和头尾初始化到一半一样也是入队操作到一半的情况。
- 已经执行过head的next指向操作(
- 队列中只有head节点,也没有获取成功的线程,因为已经初始化结束,所以head和tail指向同一个对象,
因为节点有中断,取消,超时的情况,所以这个方法无法保证返回的结果在节点状态并发变化情况下的正确性。
有必要理解一下Read fields in reverse initialization order
这个注释。网上也有人问,为什么获取tail要先于获取head呢?
本质原因还是因为初始化头尾节点也是有顺序性的,必然是cas设置head成功后,tail才会被设置。这里的读取顺序因为tail和head都是volatile修饰也是不会被重排序的。这里不详细描述各种并发情况,只假设先读head再读tail下会有问题的场景
如果是先读head再读tail,有以下这个场景会有问题,这个应该一看就明白了:
那么head为null,tail不为null,h != t
为true,然后执行(s = h.next) == null
就会空指针。那么先获取tail再获取head难道就没有问题了吗,有兴趣的同可以自己推演下倒序读取的各种场景。
实现
ReentrantLock中的FairSync类负责提供公平锁的能力,核心就是自定义的tryAcquire
方法
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 锁未被获取状态
if (c == 0) {
// 先使用hasQueuedPredecessors判断是否需要排队,返回false才进行一次cas竞争
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;
}
而公平锁的tryAcquire
方法实现的区别就是没有!hasQueuedPredecessors
的判断,其他代码一模一样。有了hasQueuedPredecessors
方法的理解,这个公平锁实现就更加深刻了。
可重入
可重入就是支持一个线程多次获取锁的能力,在释放锁的时候也需要多次释放。这个实现在Sync#nonfairTryAcquire
方法和FairSync#tryAcquire
方法中有体现,就是用if (current == getExclusiveOwnerThread())
判断如果是当前线程,就累加state。
tryRelease的实现也对可重入的逻辑进行了处理:
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;
// 只有state被减到0的时候才会设置
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
总结
本文直接对AQS源码的核心结构和源代码进行了详细的分析,然后使用ReentrantLock作为实现的同步器进行了部分了解,为后续JUC中类的源码解读打下基础。本文涉及的内容是jdk中各种同步器实现基础的核心部分,个人精力有限,不正之处望留言指出。
本文已在公众号上发布,感谢关注,期待和你交流。
AQS源码一窥-JUC系列的更多相关文章
- AQS源码二探-JUC系列
本文已在公众号上发布,感谢关注,期待和你交流. AQS源码二探-JUC系列 共享模式 doAcquireShared 这个方法是共享模式下获取资源失败,执行入队和等待操作,等待的线程在被唤醒后也在这个 ...
- AQS源码三视-JUC系列
AQS源码三视-JUC系列 前两篇文章介绍了AQS的核心同步机制,使用CHL同步队列实现线程等待和唤醒,一个int值记录资源量.为上层各式各样的同步器实现画好了模版,像已经介绍到的ReentrantL ...
- Future源码一观-JUC系列
背景介绍 在程序中,主线程启动一个子线程进行异步计算,主线程是不阻塞继续执行的,这点看起来是非常自然的,都已经选择启动子线程去异步执行了,主线程如果是阻塞的话,那还不如主线程自己去执行不就好了.那会不 ...
- JUC并发编程基石AQS源码之结构篇
前言 AQS(AbstractQueuedSynchronizer)算是JUC包中最重要的一个类了,如果你想了解JUC提供的并发编程工具类的代码逻辑,这个类绝对是你绕不过的.我相信如果你是第一次看AQ ...
- AQS源码深入分析之独占模式-ReentrantLock锁特性详解
本文基于JDK-8u261源码分析 相信大部分人知道AQS是因为ReentrantLock,ReentrantLock的底层是使用AQS来实现的.还有一部分人知道共享锁(Semaphore/Count ...
- AQS源码深入分析之共享模式-你知道为什么AQS中要有PROPAGATE这个状态吗?
本文基于JDK-8u261源码分析 本篇文章为AQS系列文的第二篇,前文请看:[传送门] 第一篇:AQS源码深入分析之独占模式-ReentrantLock锁特性详解 1 Semaphore概览 共享模 ...
- 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(二)资源的获取和释放
上期的<全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础>中介绍了什么是AQS,以及AQS的基本结构.有了这些概念做铺垫之后,我们就可以正 ...
- ReentrantLock 与 AQS 源码分析
ReentrantLock 与 AQS 源码分析 1. 基本结构 重入锁 ReetrantLock,JDK 1.5新增的类,作用与synchronized关键字相当,但比synchronized ...
- AQS源码阅读笔记(一)
AQS源码阅读笔记 先看下这个类张非常重要的一个静态内部类Node.如下: static final class Node { //表示当前节点以共享模式等待锁 static final Node S ...
随机推荐
- <数据结构>BinarySearchTree的基本操作总结
目录 ADT 结构声明 核心操作集 各操作实现 Find(),Insert(),Delete() Traverse():前中后.层 ADT 结构声明 #include<stdio.h> # ...
- 论文翻译:2020_Acoustic Echo Cancellation Based on Recurrent Neural Network
论文地址:https://ieeexplore.ieee.org/abstract/document/9306224 基于RNN的回声消除 摘要 本文提出了一种基于深度学习的语音分离技术的回声消除方法 ...
- SpringCloud创建Gateway模块
1.说明 本文详细介绍Spring Cloud创建Gateway模块的方法, 基于已经创建好的Spring Cloud父工程, 请参考SpringCloud创建项目父工程, 和已经创建好的Eureka ...
- 2 - 基于ELK的ElasticSearch 7.8.x技术整理 - java操作篇 - 更新完毕
3.java操作ES篇 3.1.摸索java链接ES的流程 自行创建一个maven项目 3.1.1.依赖管理 点击查看代码 <properties> <ES-version>7 ...
- nalu,在java中使用lambda查询数据库
不忘初心 最开始接触写代码的时候,用的是C井,查数据库直接硬编码sql,挺难受的. 后来学习到EntityFramework,用起来是真香,都是强类型,各种智能提示,代码写起来极度舒适,效率起飞. 最 ...
- Selenium_使用Select类对象处理下拉框(15)
select标签的下拉框可以使用selenium的 Select模拟下拉框选择操作. Select需要导入才能使用,导入路径如下 from selenium.webdriver.support.ui ...
- Python_paramiko-与linux交互
一.基础功能介绍 # coding=utf-8 import paramiko from time import sleep # 建立通信 transport = paramiko.Transport ...
- web.xml文件配置模板
直接贴完整代码,当然,spring的核心控制器依赖包需要通过mean提前配置 <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.// ...
- Echart可视化学习(六)
文档的源代码地址,需要的下载就可以了(访问密码:7567) https://url56.ctfile.com/f/34653256-527823386-04154f 柱状图定制 官网找到类似实例, 适 ...
- layui父表单获取子表单的值完成修改操作
最近在做项目时,学着用layui开发后台管理系统. 但在做编辑表单时遇到了一个坑. 点击编辑时会出现一个弹窗. 我们需要从父表单传值给子表单.content是传值给子表单 layer.open({ t ...