Java AQS nonsense

Foreword

For readers: At least 3 years experience of students

Speaking concurrent programming, basically think of the JDK JUC kit, which includes locks, concurrency tools, atomic class, thread pool, as well as blocking queue, which is from the Internet to find a general knowledge.

image

I believe these tools readers have seen and used part of such CountDownLatch, thread pool, atom type, but may not understand the principles involved, and the interview may require a little higher, asked to name their principles, or often there is such a question If you, how would you go to achieve.

This paper stresses the realization of AQS, you need to have the following basic

  • Use a doubly linked list queue implementation, add nodes and delete nodes operation
  • In the case of multi-threaded, doubly linked list of add or remove nodes have thread-safety issues, resulting in an infinite loop or node is empty and you can think of this scene
  • Design patterns, mainly template method pattern, this simple
  • Understand the volatile keyword, the main variable is immediately visible between main memory and thread a copy, it does not guarantee atomicity, if you want to get to the bottom, need to see the JVM memory model
  • Six states thread, mainly to the difference between waiting and timed-waiting, sleep and wait methods and await Condition methods are the thread enters timed-waiting, UNSAFE.park (thread) is a local method call, no need to obtain a lock that is You can thread into the waiting state
  • CAS stands for compareAndSwap, its parameters are typically (objects, compare properties, the expected value, the target value), this means that the object's properties do change operation, if the expected value and the same memory value, the memory value is modified to target value, it is a cpu primitive, can guarantee atomicity
  • Interrupt (interrupt) meaning, it will only interrupt blocking that exit the blocked state, and does not interrupt the running thread, it is possible we are often thrown InterruptException to misunderstand.
  • There is preferably used CountDownLatch ReentrantLock such class or AtomicXX

main content

本文参考自 https://www.cnblogs.com/waterystone/p/4920797.html 个人不太喜欢贴源码的风格,把一个很简单的东西说得很复杂了。

AQS是AbstractQueuedSynchronizer的简称,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,如下图所示。

就是多个线程对共享资源 state 的获取与释放,AQS 在顶层维护了一个线程队列,它实现了线程的出队,入队,唤醒,和节点状态的维护,使用模板方法模式,为子类提供了一些有用的方法,子类只要实现资源的获取与释放即可。

它有两对方法分别用于独占资源和共享资源 acquire-release,acquireShared-releaseShared

下面 -- 标记的都是方法名,可以自己去源码中查看,如果当前方法不清楚含义可以来这里看

下面的流程一定要对照源码 AbstractQueuedSynchronizer 来查看,不然可能不知道我在说什么,这里说的是独占模式和共享模式资源的获取和释放,这个框架只会来管你有几个资源,不会管你资源的其它属性。

获取独占资源的流程是这样子的 -- acquire

  1. 先尝试获取资源,如果获取到资源了,直接返回 -- tryAcquire
  2. 如果未获取到资源,则创建一个节点添加到队列尾 -- addWaiter
  3. 以自旋的方式判断是否排队排到了第一个,但虽然你排到了第一个,也要由子类的 tryAcquire 来决定你能不能获取资源 -- acquireQueued
  4. 如果不能获取资源,则先让前面的弄完了通知我一下 -- shouldParkAfterFailedAcquire,我先休息下(这时候线程阻塞处于 waiting 状态) -- parkAndCheckInterrupt

释放独占资源的流程是这样子的 -- release

  1. 先尝试释放资源 -- tryRelease
  2. 如果释放成功,从队列后面往前找,找到队列最前面那个正在等待的家伙,叫醒它; 这里如果认真看了一定会有一个疑问,为什么要从队列后面找,从前面找不是更快吗,文章末尾给出答案 ,这里你理解流程和使用 -- unparkSuccessor

tryAcquireShared 的含义是,如果返回的结果小于 0 ,表示没有可用资源,如果等于 0 表示最后一个可用资源,如果大于 0 ,表示获取成功并且还有可用资源

获取共享资源流程是这样子的 -- acquireShared

  1. 先尝试获取共享资源,如果成功直接返回 -- tryAcquireShared
  2. 如果获取失败,以共享模式添加到队尾 -- addWaiter
  3. 以自旋的方式判断是否排队排到了第一个,但虽然你排到了第一个,也要由子类的 tryAcquireShared 来决定你能不能获取资源 -- doAcquireShared
  4. 如果获取到资源了,但是资源还有剩余,叫醒后面的弟兄 -- setHeadAndPropagate
  5. 如果不能获取资源,则先让前面的弄完了通知我一下 -- shouldParkAfterFailedAcquire,我先休息下(这时候线程阻塞处于 waiting 状态) -- parkAndCheckInterrupt

释放共享资源流程

  1. 先尝试释放资源 -- tryReleaseShared
  2. 如果释放成功,从队列后面往前找,找到队列最前面那个正在等待的家伙,叫醒它 --doReleaseShared

节点状态 waitStatus

​ 这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • 0:新结点入队时的默认状态。

注意:负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。

在独占模式中只需要关注 3 个状态 > 0 的为取消,0 是默认就是这个状态 < 0 是可唤醒后继节点

学习这个不能只看这个队列,因为它是抽象出来的一个框架,需要结合具体的使用示例,如 CountDownLatch ,去看它是如何工作的。

CountDownLatch 构造了一个有 n 个资源的同步队列,使用共享模式,因为会有多个线程同时访问资源,每当 countDown 的时候,去释放了一个资源,然后在主线程 await 的时候去获取一个共享资源,但这个获取资源是只有资源量为 0 的时候才是成功的,核心代码就一句话。

protected int tryAcquireShared(int acquires) {
 return (getState() == 0) ? 1 : -1;
}

再看看独占锁的使用 ReentrantLock ,它使用独占资源,同一时刻只能有一个线程访问共享资源,增加了一个属性 OwnerThread ,当再次是同一个线程获取锁的时候是可重入的,有公平锁和非公平锁的实现,非公平锁在进行 lock 的时候 ,会先去试图将 state 设置为 1 ,预期为 0 ,如果成功,则成功插队,公平锁在 tryAcquire 增加了一个判断 hasQueuedPredecessors 如果有线程在排队就乖乖排队吧,并且没有在 lock 的时候尝试去占用资源 。

这里解释下在释放资源的时候 unparkSuccessor 方法为什么要从队尾开始找,从前面开始找不是更快吗

要理解这个,你首先得理解双向队列是如何插入节点的,假设有这样一个队列

head = A <---> B = tail

需要往队列尾加入节点 C ,应该是这样操作的

C.prev = tail;
tail.next = C;
tail = C;

在多线程并发情况下,这里多步操作并不是原子性的,tail 属于临界资源随时可能被其它线程修改成其它指向。

假设这种场景 C 在入队的时候 ,进行 tail.next =C 这一步前 ,D 也在入队,如果 C 先完成,这时队列结构会变成这样子

head = A <---> B <---> C

​ B <---> D = tail

AQS 使用了自旋和 CAS 来解决这个麻烦 ,它的代码是这样子的,在 enq 函数中

 for (;;) {
     Node t = tail;
     if (t == null) { // Must initialize
         if (compareAndSetHead(new Node()))
             tail = head;
     } else {
         node.prev = t;
         if (compareAndSetTail(t, node)) {
             t.next = node;
             return t;
         }
     }
 }

这个函数的主要作用是当那[一瞬间]在队列为空的时候进入这个方法创建队列,有可能在正想初始化队列的时候已经被其它线程初始化了,所以初始化时使用了 CAS compareAndSetHead(),然后再把 tail 的指向和 head 的指向一致,这里因为另一个线程不能初始化成功,所以这里来说是正确的。

Look else, when the other thread has completed initialization, tail will not empty when entering here, this simply means t this moment the end node, the end node may be modified by other threads to use the guarantee only if the CAS compareAndSetTail Last node is the end node of the transient time can modify node.prev = t, to ensure that pre-node node must be correct, but not necessarily the home node, if before t.next = node assignment tail is modified, the queue will be very likely to become this way

Assuming concurrent addition of the node C, D, E

head = A <---> B <---> C <--- D <--- E = tail

​ B ---> D C ---> E

As can be seen from this figure, finally, to find a list from the back is continuous, but later went looking for is not necessarily answer the question very beginning.

Written a bit obscure, I also want to write the most user-friendly way, but limited expression levels, look great God correction.

Little promotion

Writing is not easy, I hope the support of open source software, and my gadgets, welcome to gitee point star, fork, put bug.

Excel common import and export, support Excel formulas
blog address: https://blog.csdn.net/sanri1993/article/details/100601578
gitee: https://gitee.com/sanri/sanri-excel-poi

Use the template code, the gadget generate code from the database, and some projects can often be used in the
blog address: https://blog.csdn.net/sanri1993/article/details/98664034
gitee: https://gitee.com/ sanri / sanri-tools-maven

Guess you like

Origin www.cnblogs.com/sanri1993/p/12093697.html
AQS
AQS
Recommended