[9036期] JUC多线程- AQS抽象队列同步器原理
AQS的工作原则
什么是 AQS
s,抽象排队同步器,抽象队列同步器,是J.U.C .中锁定和同步组件的基础,工作原理是如果被请求的共享资源空闲,则将当前被请求资源的线程设置为有效工作线程,并将共享资源设置为锁定状态。如果请求的共享资源被占用,那么无法获得锁的线程将被添加到等待队列中。此时,当线程阻塞等待和唤醒时,我们需要一个锁分配机制,而 AQS 是通过 CLH 队列实现锁分配的机制.
CLH 同步队列的模型
CLH队列是一个由内部类Nodes组成的同步队列,是一个双向队列(没有队列实例,只有Nodes之间的关系)。将请求共享资源的线程封装为一个节点节点,实现锁分配。同时,内部类ConditionObject用于构造等待队列。当调用ConditionObject的await()方法时,线程将加入等待队列。当调用ConditionObject的signal()方法时,线程将从等待队列移动到同步队列进行锁竞争。AQS只能有一个同步队列,但可以有多个等待队列。AQS CLH同步队列模型如下:
AQ有三个主要变量,即头、尾和状态,其中头指向同步队列的头,注意头是一个空节点,不存储信息。尾部是同步队列的尾部,为了方便查找队列,同步队列采用双向链表的结构。当Node节点设置为head时,其线程信息和前置节点都会被清除,因为线程已经获取了同步状态,正在执行,所以不需要存储相关信息。head只需要保存后继节点的指针,便于head节点在释放同步状态后唤醒后继节点。
的入队和出队操作是无锁操作。基于CAS自旋锁的实现,AQS保持一个易失性修改的int型状态同步状态。volatile保证线程之间的可见性,通过CAS对同步状态进行原子操作,修改其值。当state=0时,意味着没有线程持有共享资源的锁。当state=1时,意味着一些线程当前正在使用共享变量,其他线程必须加入同步队列等待。
内部类节点数据结构分析
静态最终类节点
//共享模式
静态最终节点SHARED=new Node();
//独占模式
静态最终节点EXCLUSIVE=null
//标识线程处于结束状态。
静态最终int CANCELED=1;
//等待被唤醒
静态最终int SIGNAL=-1;
//条件状态
静态最终int CONDITION=-2;
//通过使用共享模式中的表示获得的同步状态将被传播。
静态最终int PROPACT=-3;
//等待状态,有取消、信号、条件和传播四个值。
可变整数等待状态;
//同步队列中的前置节点
易失节点前一个;
//同步队列中的后续节点
易失节点下一个;
//请求锁定的线程
线程易失性线程;
//等待队列中的后续节点,与条件相关,稍后分析。
节点下一个服务员;
//确定是否为共享模式。
最终布尔值Isshared(){ 0
return nextWaiter==SHARED
}
//.
}
AQS可以分为两种模式:独占模式和共享模式。例如,可重入锁和循环屏障是基于独占模式实现的,而信号量和计数监视是基于共享模式实现的。
waitStatus变量指示当前封装为节点节点的线程的等待状态,有四个值:取消、信号、条件和传播:
取消:值为1表示在同步队列中等待的线程已经超时或被中断,并且处于结束状态,因此需要从同步队列中删除Node节点。
信号:值-1表示
继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为 SIGNAL,当该节点释放了同步锁之后,就会唤醒该节点的后继节点
AQS 的设计模式
AQS 的模板方法模式
AQS 的基于模板方法模式设计的,在 AQS 抽象类中已经实现了线程在等待队列的维护方式(如获取资源失败入队/唤醒出队等),而对于具体共享资源 state 的获取与释放(也就是锁的获取和释放)则交由具体的同步器来实现,具体的同步器需要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源,只有用到 condition 才需要去实现它
- tryAcquire(int):独占模式,尝试获取资源,成功则返回 true,失败则返回 false
- tryRelease(int):独占方式,尝试释放资源,成功则返回 true,失败则返回 false
- tryAcquireShared(int):共享方式,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
- tryReleaseShared(int):共享方式,尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false
JUC 中提供的同步器
- 闭锁 CountDownLatch:用于让主线程等待一组事件全部发生后继续执行。
- 栅栏 CyclicBarrier:用于等待其它线程,且会阻塞自己当前线程,所有线程必须全部到达栅栏位置后,才能继续执行;且在所有线程到达栅栏处之后,可以触发执行另外一个预先设置的线程。
- 信号量 Semaphore:用于控制访问资源的线程个数,常常用于实现资源池,如数据库连接池,线程池。在 Semaphore 中,acquire 方法用于获取资源,有的话,继续执行
- 没有资源的话将阻塞直到有其它线程调用 release 方法释放资源;
- 交换器 Exchanger:用于线程之间进行数据交换;当两个线程都到达共同的同步点(都执行到exchanger.exchange 的时刻)时,发生数据交换,否则会等待直到其它线程到达;
CountDownLatch 和 CyclicBarrier 的区别
两者都可以用来表示代码运行到某个点上,二者的区别在于:
① CyclicBarrier 的某个线程运行到某个位置之后就停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 的某线程运行到某个位置之后,只是给计数值-1而已,该线程继续运行;
② CyclicBarrier 可重用,CountDownLatch 不可重用,计数值 为 0 时该 CountDownLatch 就不可再用了。
ReentranLock 中独占模式下非公平锁的获取流程
获取独占锁的过程是定义在 tryAcquire() 中的,当前线程尝试获取同步状态,如果获取失败,就将线程封装成 Node 节点插入到 CLH 同步队列中。插入同步队列后,线程并没有放弃获取同步状态,而是根据前置节点状态状态判断是否继续获取,如果前置节点是 head 结点,继续尝试获取,否则就将线程挂起。如果成功获取同步状态则将自己设置为 head 结点。当持有同步状态的线程释放资源后,也会唤醒队列中的后继线程。
ConditionObject 阻塞队列
什么是 Condition 接口
AQS 的阻塞队列是基于内部类 ConditionObject 实现的,而 ConditionObject 实现了 Condition 接口。那 Condition 接口是什么呢Condition 主要用于线程的等待和唤醒,在JDK5之前,线程的等待唤醒是用 Object 类的 wait/notify/notifyAll 方法实现的,这些方法必须配合 synchronized 关键字使用,使用起来不是很方便,为了解决这个问题,在 JDK5 之后,J.U.C 提供了Condition。
- Condition.await 对应于 Object.wait;
- Condition.signal 对应于 Object.notify;
- Condition.signalAll 对应于 Object.notifyAll;
与 synchronized 的等待唤醒机制相比,Condition 能够精细的控制多线程的休眠与唤醒,具备更多的灵活性, 通过多个 Condition 实例对象建立不同的等待队列,从而实现同一个锁拥有多个等待队列。而 synchronized 关键字只能有一组等待唤醒队列,使用 notify() 唤醒线程时只能随机唤醒队列中的一个线程。
ConditionObject 阻塞队列实现原理
Condition 的具体实现之一是 AQS 的内部类 ConditionObject,每个 Condition 都对应着一个等待队列,也就是说如果一个锁上创建了多个 Condition 对象,那么也就存在多个等待队列。当调用 ConditionObject 的 await() 方法后,线程将会加入等待队列中,当调用 ConditionObject 的 signal() 方法后,线程将从等待队列转移动同步队列中进行锁竞争。AQS 的 ConditionObject 中的等待队列模型如下:
AQS 的线程唤醒机制原理
AQS 的线程唤醒是通过 singal() 方法实现的,我们先看下 singal() 方法线程唤醒的流程图:
signal() 方法主要调用了 doSignal(),而 doSignal() 方法中做了两件事:
(1)从条件等待队列移除被唤醒的节点,然后重新维护条件等待队列的 firstWaiter 和 lastWaiter 的指向。
(2)将从等待队列移除的结点加入同步队列(在 transferForSignal() 方法中完成的),如果进入到同步队列失败并且条件等待队列还有不为空的节点,则继续循环唤醒后续其他结点的线程。注意:无论是同步队列还是等待队列,使用的 Node 数据结构都是同一个,不过是使用的内部变量不同罢了
所以 signal() 的流程可以概述为:
- signal() 被调用后,先判断当前线程是否持有独占锁
- 如果有,那么唤醒当前 Condition 等待队列的第一个结点的线程,并从等待队列中移除该结点,添加到同步队列中
- 如果加入同步队列失败,那么继续循环唤醒等待队列中的其他结点的线程
- 如果成功加入同步队列,那么如果其前驱结点已结束或者设置前驱节点状态为 Node.SIGNAL 状态失败,则通过 LockSupport.unpark() 唤醒被通知节点代表的线程。
到此 signal() 任务完成,被唤醒后的线程,将调用 AQS 的 acquireQueued() 方法加入获取同步状态的竞争中,这就是等待唤醒机制的整个流程实现原理。
吃水不忘挖井人:
|
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/155876.html