深入并发编程——AQS框架

Java中各类同步器

java中的同步器分为独占式和共享式,Monitor只支持独占式而AQS支持独占和共享两种模式。
基于Monitor的synchorized和基于AQS的独占式的ReentrantLock()和共享式的Semaphore、CountDownLatch、CyclicBarrier等。

AQS

什么是AQS

AQS队列同步器(AbstractQueuedSynchronizer),是Java提供的用来构建锁或者其他同步组件的基础框架。它使用了一个int的成员变量表示同步状态,通过内置的FIFO队列(CLH队列)来完成资源获取线程的排队工作。

AQS原理

(1)AQS 全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架(其余的相关工具都是基于它的,是它的子类或调用其方法)。
(2)用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁。独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源。
(3)提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList。
(4)条件变量来实现等待、唤醒机制,支持多个条件变量,类似于Monitor 的 WaitSet。
(5)继承自 AQS 的子类主要实现这样一些方法:tryAcquire,tryRelease,tryAcquireShared,tryReleaseShared,isHeldExclusively。
(6)基于 AQS 创建不可重入锁案例(同步原理 P37)
(7)早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如用可重入锁去实现信号量,或反之。这显然不够优雅,于是在 JSR166(java 规范提案)中创建了 AQS,提供了这种通用的同步器机制。
(8)AQS 要实现的功能目标:
阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire。
获取锁超时机制。
通过打断取消机制
独占机制及共享机制
条件不满足时的等待机制

AQS的独占式实现——ReentrantLock

1、ReentrantLock与Synchorized的相同点与区别

相同点:
(1)都具有临界区保护能力。
(2)都支持可重入。
(3)都提供线程间的协作。
(4)都提供锁的升级逻辑:
monitor 偏向锁 -> 轻量级锁 -> 重量级锁
AQS CAS竞争 -> 休眠+排队竞争
(5)都提供等待队列
monitor EntryList, WaitSet
AQS CLH队列
区别:Synchorized VS ReentrantLock
(1)基于Monitor VS 基于AQS
(2)非Java生态 VS Java生态
(3)不响应中断 VS 响应中断
(4)基于同步阻塞 VS 基于同步非阻塞(可以tryLock)
(5)不可配置公平锁 VS 可配置公平锁

ReentrantLock 原理

(1)使用 compareAndSetState 将当前 state 由 0 设置为 1,表示给当前NofairSync 上锁,当没有竞争时,上锁成功,使用 setExclusiveOwnerThread 将当前线程设置为 owner 线程,即加锁成功。
在这里插入图片描述
(2)当竞争出现时,即 compareAndSetState 会失败,则进入 acquire 方法,首先调用 tryAcquire 再次进行一次尝试,若再次失败则进入 addWaiter 逻辑(构造一个 node 队列)。因为此时的 head 和 tail 都是空的,所以要尝试添加节点。首次创建时会创建两个节点,其中第一个 Node 称为 Dummy 哑元,用来占位并不关联线程,第一个创建的正式节点同时会链到链表的尾部。其中黄色三角内的数字表示 Node 的 waitStatus 状态,0 为正常状态。
在这里插入图片描述
代码层面则是进入 acquireQueue 函数,函数内会做最后的挣扎尝试获取锁。acquireQueue 会进入一个死循环,循环内先获取当前传入 Node 节点的前驱节点,前驱节点如果是 head 则证明当前节点处于第二位,处于第二位就还有机会再通过 tryAcquire 再获取一次锁,若再次失败则进入判断shouldParkAfterFailedAcquire()方法,将前驱 node,即 head 的 waitStatus 改为-1,初次改为-1 时函数返回 false。改为-1 表示当前节点有责任唤醒后继结点。在这里插入图片描述
(此操作的目的是当前结点进入 park 阻塞之前告诉自己前面的节点有责任唤醒自己),初次由于返回的是 false 则会再进入一次循环,再次运行到shouldParkAfterFailedAcquire()方法会返回 true,即开始运行&&之后的parkAndCheckInterrupt()逻辑。综上,thread 竞争失败后依然会进行多次尝试,直到大概第四次失败时才会进入 park 逻辑阻塞住,变成图中灰色的样子。在这里插入图片描述
(3)当再次发生多个竞争时,变成如下情况:在这里插入图片描述
(4)当第一个线程释放锁时,进入 tryRelease 方法,即把 OwnerThread 设为 null,把 state 设为 0。检查队列是不是不为空,再检查 head 的 waitStatus是不是不等于 0,若成立则代表应该唤醒 head 的下一个节点,此时进入unparkSuccessor 函数。找到离 head 最近的一个 node 并 unpark 使其恢复运行。则其会再进行一次循环,此时发现它的前驱节点是 head 并进入 tryAcquire 方法,若 tryAcquire 返回 true 则证明锁获取成功。使当前线程成为 owner,将当前节点设为头节点并断开前驱节点与当前节点的连接(p.next=null 使原本的 head从链表断开,帮助垃圾回收),同时将节点内容设置为 null,成为 Dummy 节点。
在这里插入图片描述
即如果加锁成功(没有竞争),会设置 exclusiveOwnerThread 为 Thread-1,state = 1,head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread 原本的 head 因为从链表断开,而可被垃圾回收。
在这里插入图片描述
(6)如果此时有其它线程来竞争(非公平的体现),且恰好其它线程获得了锁,则 Thread-1 又会进入 acquireQueue 流程 park 阻塞住。
在这里插入图片描述
(7)可重入原理:如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入,将 state++,解锁的时候只有 state 减到 0 才释放成功。
(8)可打断原理:不可打断模式下被打断后 interrupted 打断标记置为 true,会在下次获取到锁时将 interrupted 标记返回。即仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了。可打断模式下被 interupt 会抛出异常而不是继续循环。
(9)公平锁实现原理:判断头部和尾部是否相等,若不相等则证明有节点,队列中优先级最高的是老二节点,如果队列中没有老二或老二不是当前线程则不会执行当前线程。
(10)ReentrantLock 中每个条件变量都对应一个 ConditionObject 对象,也维护了一个列表用来存放条件不满足需要 wait 的线程。使用 firstWaitor 和lastWaitor 来维护。创建新的 Node 状态为 -2,关联 Thread-0,加入等待队列尾部使其 park 住。接下来执行 AQS 的 fullRelease 流程,释放同步器上的锁。唤醒等待队列中的下一个节点。
在这里插入图片描述
(11)signal 原理:进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node,若当前节点有下一个节点则将 head的下一个节点设为当前节点的下一个节点,若没有则设置为 null。转移等待队列中的线程到 NonfairSync 的 head 列表中,转移工作包括先检测其添加后的前一个节点 waitStatus 状态是否为 0(因为),若小于 0 则将其更新为-1。若更新成功或其本身为 1(该节点被取消)则 unpark 该线程并 return true 表示成功。

Semaphore 信号量

(1)作用:用来限制访问共享资源的线程上限。
(2)semaphore 应用 1:使用 Semaphore 限流,在访问高峰期,让请求线程阻塞,高峰期过去再释放许可。当然它只适合限制单机模式下的线程数量(分布式系统不适用),而且仅是限制线程数,而不是限制资源数(例如连接数量 Tomcat 中使用 LimitLatch 来实现)。
(3)semaphore 应用 2:实现简单连接池(适用于线程数和连接数相同的情况),对比享元模式下的实现(wait 和 notify),性能和可读性显然更好。在这里插入图片描述
(4)原理:传入的信号量大小赋值给 state。当多个线程来竞争信号量保护的资源时,进入 tryAcquireShared 中尝试获取权限,若此时剩余量等于 0 了则直接返回一个负数,当资源数满了无法再获取时,会进入doAcquireSharedInterruptibly,先创建一个用来占位的头节点,再创建当前线
程关联的节点,获取当前节点的前驱节点,若为 head 则再尝试一次 acquire,失败则进入 shouldParkAfterFailedAcquire 方法再次尝试获取锁,失败则将其前面的节点 state 设为-1 表示有责任唤醒后继结点,并再次尝试(类似ReentrantLock)。
在这里插入图片描述
Release 流程:首先释放时会将信号量的值加一,然后进入doReleaseShared 阶段,先检查头节点的 state 是否为-1,若是则 cas 尝试将其改成 0,若成功则唤醒头节点之后的节点,让其 unpark 继续进行进入循环,执行到 tryAcquireShared 方法尝试获取信号量权限,若成功则将自己设为头节点并设为 null,同时递归唤醒自己后继的节点,这样每个节点都会被唤醒,但是并不是都会获取到权限,当信号量再次满了就会进入 park。

CountDownLatch 倒计时

(1)倒计时结束后才会从其阻塞处继续向下运行。
(2)CountDownLatch 比 join 的优点是在使用线程池时不能使用 join 等待线程结束,因为线程池设计的目的就是重用线程完成多个任务,所以应该使用CountDownLatch 在某一任务结束后手动减去任务量。

Cyclicbarrier 循环栅栏

(1)描述:当几个任务使用 CountDownLatch 使几个任务循环运行多轮时只能在循环中重复创建多个 CountDownLatch,不能直接设置 CountDownLatch。
(2)解决:CyclicBarrier 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置计数个数,每个线程执行到某个需要“同步”的时刻调用await() 方法进行等待,当等待的线程数满足计数个数时,继续执行。其传入的第二个参数是一个 lambda 函数,可以用来处理同步后要执行的处理函数。当CyclicBarrier 计数减为零后会自动恢复成初始状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值