关于java:Java并发编程-Lock-Condition-ReentrantLock一

48次阅读

共计 4302 个字符,预计需要花费 11 分钟才能阅读完成。

Lock 框架为 java 并发编程提供了除 synchronized 之外的另外一种抉择。synchronized 是隐式实现,底层封装了对锁资源的获取和开释的所有实现细节,程序员不须要关怀也没有方法关怀这些细节,应用起来十分不便也十分平安。

而 Lock 由 java 语言实现,公开了锁资源获取和开释的所有细节,在资源锁定过程中提供了更多选项,在获取锁资源后,能够通过 Condition 对象对锁资源做细粒度的治理。

最要害的是 Lock 大量应用了 CAS,充分利用“持有锁的线程不会长时间占用锁”这一假如,有可能的状况下就尽量先自旋、后锁定资源。所以多线程环境下 Lock 应该比 synchronized 有更好的性能。

java 线程池框架 Executor 中大量应用了基于 Lock 接口的 ReentrantLock,把握 ReentrantLock 是深刻了解各种 Executor(ThreadPoolExecutor、ScheduledThreadPoolExecutor 等)以及各种阻塞队列的必要前提。

Lock 有独占锁、共享锁的区别,独占锁是指某一线程获取锁资源后即独占该锁资源、其余线程只能期待,共享锁是指多个线程能同时取得锁资源。

明天咱们的钻研对象是 ReentrantLock,ReentrantLock 是独占锁,次要钻研内容:

  1. ReentrantLock 的基本概念
  2. 根底数据机构:AQS,CLH 队列
  3. 偏心锁、非偏心锁
  4. Condition
  5. 没有 Condition 参加的 lock、unlock
  6. 有 Condition 参加的 lock、unlock

ReentrantLock 的基本概念

顾名思义,ReentrantLock 是“可重入锁”,意思是同一线程能够屡次取得锁,n 次取得须要 n 次开释能力最终开释掉 ReentrantLock。

ReentrantLock 的基本原理:

  1. 与 synchronized 不同,ReentrantLock 不存在“锁对象”的概念,或者能够了解为锁对象就是 ReentrantLock 对象自身
  2. ReentrantLock 设置一个状态值,通过对状态值的原子操作实现对锁资源的获取和开释,任何一个线程能获取锁资源的充沛必要条件是 ReentrantLock 处于闲暇状态,同理,任何一个线程取得锁资源后 ReentrantLock 即处于占用状态
  3. ReentrantLock 的两个最根本的操作:lock 和 unlock,lock 获取锁资源,unlock 开释锁资源
  4. ReentrantLock 保护一个 CLH 队列,CLH 队列是一个先进先出的双向队列
  5. ReentrantLock 处于闲暇状态则 lock 调用立刻返回,调用线程取得锁资源。否则,申请线程进入 CLH 队列排队,期待被其余线程唤醒
  6. 取得锁资源的线程在业务执行实现后调用 unlock 开释锁资源,之后以 FIFO 的准则唤醒最先进入队列排队的线程
  7. 被唤醒的线程继续执行 lock 操作,节点从 CLH 队列出队,返回 — 意味着申请锁资源的线程在期待后获取锁资源胜利,持续第 6 步的逻辑

以上是没有 Condition 对象参加的 ReentrantLock 的获取、开释锁资源的逻辑,绝对比较简单。

有 Condition 参加的时候,状况会略微简单一点:

  1. ReentrantLock 对象能够通过 new Condition()操作持有 Condition 对象,一个 ReentrantLock 能够持有多个 Condition 对象
  2. Condition 保护一个 Condition 队列
  3. Condition 的罕用的操作包含 await、signal 等,执行操作的时候假如以后线程曾经获取到了 ReentrantLock 锁资源
  4. await 操作会开释掉以后线程曾经获取到的 ReentrantLock 锁资源、挂起以后线程,并且将以后线程退出 Condition 的队列排队期待被其余线程唤醒。比方 DelayedWorkQueue 的 take 办法中,如果以后 DelayedWorkQueue 队列空的话,则 take 线程退出到命名为 available 的 Condition 中排队等待
  5. 当相干操作可能导致 Condition 的条件满足的时候,调用 Condition 的 signal 办法唤醒在 Condition 队列中期待的线程。比方上例中 DelayedWorkQueue 的 add 办法实现之后,调用 available 的 signal 办法,唤醒在 available 队列中排队等待的线程。
  6. 线程被唤醒之后从 Condition 队列出队,进 ReentrantLock 的 CLH 队列排队期待从新获取锁资源

Condition 举例:take 办法中队列空的话,挂起期待

Condition 举例:offer 办法中写入队列后,唤醒期待的线程

对 ReentrantLock 应该有一个根本的意识了,如果只是想要对 ReentrantLock 做一个根本理解、可能看懂 ReentrantLock 的利用、而不是要从源码角度做深入研究的话,集体认为把握下面这些基本原理应该就够了,保障能看懂阻塞队列、线程池中的无关 ReentrantLock 的源码逻辑了。

然而如果想要彻底搞清楚 ReentrantLock 到底是怎么实现以上逻辑的,就须要从源码角度持续做深入研究了。

ReentrantLock 数据结构:AQS 及 CLH 队列

多个线程同时竞争 ReentrantLock 锁资源的时候,只能有一个竞争获胜的线程取得锁资源、其余线程就只能排队期待。这个用来排队的队列就是 CLH 队列,AQS(AbstractQueuedSynchronizer)是实现 CLH 队列的虚构类。

ReentrantLock 有一个十分重要的属性 Sync,Sync 是 AQS 的虚构扩大类,Sync 有两个实现类:NonfairSync 和 FairSync,类构造如下:

NonfairSync 和 FairSync 都是 AQS 的最终实现,AQS 虚构类是一个规范模板,定义了 Lock 锁的根本数据结构(阻塞队列)、并实现了 Lock 的绝大部分性能。

进入队列排队的线程被封装为Node,Node 是 AQS 定义的外部类,是咱们学习 AQS 首先要把握的内容。

Node 的重要属性:

waitStatus:期待状态,Node 就是用来排队的,waitStatus 就代表以后节点的期待状态,有以下几种期待状态:

  1. CANCELLED = 1:示意以后期待线程曾经被 calcel 掉了
  2. SIGNAL = -1:示意该节点是在 CLH 队列中排队期待出队
  3. CONDITION = -2:示意以后节点是在 Condition 队列中期待出队
  4. PROPAGATE = -3:共享锁会用到,暂不剖析

prev:上一节点
next: 双向队列嘛,当然也要有下一节点
Thread thread: 节点的配角,排队线程
nextWaiter:Condition 队列专用,用来指向 Condition 队列的下一节点

AQS 的同步队列(CLH)以及 Condition 队列的节点都是用这个 Node,所以 Node 类做了一部分针对两者的兼容设计,比方 nextWaiter 是针对 Condtion 队列的下一节点,next 是针对 CLH 的下一节点。

AQS 重要属性

state:锁状态,通过对 state 的原子操作实现对锁资源的管制:某一线程通过原子操作胜利将 state 从闲暇批改为占用则意味着以后线程胜利取得了锁资源。无奈取得锁资源的线程则封装为 Node 节点进入队列排队期待。

head:首节点,头节点
tail: 尾结点

通过 head 节点、tail 节点,以及每个节点的 prev、next,AQS 实现了一个双向队列。

偏心锁和非偏心锁

所谓的偏心锁和非偏心锁就是由 Sync 属性决定的:当 Sync 创立为 NonfairSync 的时候,就是非偏心的 ReentrantLock,否则就是偏心的 ReentrantLock。

应用无参结构器创立的是非偏心 ReentrantLock,有参结构器 ReentrantLock(boolean fair)能够通过参数指定创立偏心还是非偏心锁。

偏心锁在线程申请锁资源的时候会查看 CLH 队列,队列不空的话首先进入队列排队,先提出申请的线程会优先取得锁资源,因而是“偏心”的锁。

非偏心锁在线程申请锁资源的时候不会查看 CLH 队列,间接尝试取得锁资源,获取失败后才进入队列排队。所以申请线程会失去比队列中的线程更高的优先级,对于队列中排队的线程来说是不偏心的,所以叫非偏心锁。

Condition

Condition 提供 await 和 signal(以及他们的变种)办法为 ReentrantLock 锁资源提供更多抉择:以后线程获取到 ReentrantLock 锁资源后,能够通过 Condition 对象的 await 办法挂起以后线程直到其余线程通过该对象的 signal 办法唤醒。

一个 ReentrantLock 能够创立多个 Condition 对象,每一个 Condition 对象都是独立的、互不影响。ReentrantLock 好比是一条街上的黑社会老大,黑社会老大首先要把这条街拿下,也就是取得 ReentrantLock 锁资源。之后的每一个 Condition 好比是这条街道上的饭店 A、小卖店 B、公共卫生间 C,别离对应 ConditionObjectA、ConditionObjectB、ConditionObjectC,失去黑社会老大容许后你就能够随便进出饭店吃饭了,然而如果饭店客满了,就必须通过调用 ConditionObjectA 的 await 办法进入到 ConditionObjectA 的队列中排队期待(以后线程封装为 AQS 中的 Node 进入队列(假如叫 NodeA),以后线程 A 挂起),此时黑社会老大须要交出对整条街的锁权限(貌似不太正当 …),尔后饭店 A 有人吃完了要离店,就会通过 ConditionObjectA 的 signal 办法告诉正在队列中排队等待的 NodeA,于是 NodeA 从 ConditionObjectA 队列中进去,到 ReentrantLock 的 CLH 队列中排队、期待从新获取 ReentrantLock 锁资源之后再唤醒线程 A。这个过程中如果有其他人(其余线程)要进入小卖店 B,须要进行操作的就是小卖店对应的 ConditionObjectB,和饭店对应的 ConditionObjectA 没有任何关系。

小结

发现开篇定下的内容太多了,篇幅所限,前面的“没有 Condition 参加的 lock、unlock”以及“有 Condition 参加的 lock、unlock”,根本就是上述逻辑的源码剖析,放在下一篇。

Thanks a lot!

上一篇 周期性工作线程池 – ScheduledThreadPoolExecutor & DelayedWorkQueue

正文完
 0