AQS源码一窥-JUC系列

2024-08-31 14:08
文章标签 源码 系列 juc aqs 一窥

本文主要是介绍AQS源码一窥-JUC系列,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Python微信订餐小程序课程视频

https://edu.csdn.net/course/detail/36074

Python实战量化交易理财系统

https://edu.csdn.net/course/detail/35475

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秒*/@Overridepublic 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) {// 创建NodeNode 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是否为空就相当于判断了初始化是否结束。

下图是这种场景同步队列节点变化情况:
image

  • 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 GCfailed = 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) {// 前节点的waitStatusint 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没有做自旋操作,是简单几个赋值操作集合,因为这个方法是确保tryAcquiretryAcquireShared成功后执行的,所以不需要考虑并发情况。方法中会把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状态了。

延续前面的测试代码,继续图解节点数据的变化:
image-20220201233215428

补充说明:

因为测试代码是一个线程获取锁,一个线程等待,所以队列中只会有两个节点一个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 existif (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设置一次为SIGNALif (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执行,节点数据的变化如下图:
image-20220203115247292

唤醒后节点线程

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返回成功,然后开始替换头节点。

队列节点数据变化如图:

image-20220203163342984

ReentrantLock源码

ReentrantLock基于AQS实现的可重入锁,支持公平和非公平。

进行了前面AQS代码的解析,ReentrantLock的代码变得异常简单,考虑到篇幅有限,下面只对公平性和可重入性进行解析,在后续文章中的还会再使用ReentrantLock。

公平/非公平

ReentrantLock内部实现了一个内部抽象类Sync,它的子类有FairSyncNonfairSync,看名字就明白了具体公平和非公平就是这两个类的实现不同了。

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 orderNode h = head;Node s;return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
}

截取部分方法注释:只要有线程等待的时间比当前线程长就应该返回true,否则返回false。

虽然这个方法的判断代码不多,可是直接看会有点懵,但是有了前面的代码解析铺垫,这个代码瞬间看懂。

一个关键的关联信息是前面介绍的enq方法中初始化头尾节点,我们已经知道初始化头尾节点不是原子操作,分成两步操作:

   compareAndSetHead(new Node()) // 1tail = 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,这个场景和头尾初始化到一半一样也是入队操作到一半的情况。

因为节点有中断,取消,超时的情况,所以这个方法无法保证返回的结果在节点状态并发变化情况下的正确性。

有必要理解一下Read fields in reverse initialization order这个注释。网上也有人问,为什么获取tail要先于获取head呢?

本质原因还是因为初始化头尾节点也是有顺序性的,必然是cas设置head成功后,tail才会被设置。这里的读取顺序因为tail和head都是volatile修饰也是不会被重排序的。这里不详细描述各种并发情况,只假设先读head再读tail下会有问题的场景

如果是先读head再读tail,有以下这个场景会有问题,这个应该一看就明白了:

image-20220209171433212
那么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中各种同步器实现基础的核心部分,个人精力有限,不正之处望留言指出。

本文已在公众号上发布,感谢关注,期待和你交流。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A04tK683-1644444535009)(https://images.cnblogs.com/cnblogs_com/killbug/2038380/o_210928142639qrcode_for_gh_587c1ab9714d_344.jpg%0A)]

这篇关于AQS源码一窥-JUC系列的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1124114

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显