Java并发编程 Lock Condition & ReentrantLock(一)

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

#eentrantLock的基本概念
顾名思义,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方法中队列空的话,挂起等待

Java并发编程 Lock Condition & ReentrantLock(一)_第1张图片
Condition举例:offer方法中写入队列后,唤醒等待的线程
Java并发编程 Lock Condition & ReentrantLock(一)_第2张图片

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

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

ReentrantLock数据结构:AQS及CLH队列

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

ReentrantLock有一个非常重要的属性Sync,Sync是AQS的虚拟扩展类,Sync有两个实现类:NonfairSync和FairSync,类结构如下:
Java并发编程 Lock Condition & ReentrantLock(一)_第3张图片

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:node类型,首节点,头节点
tail:node类型,尾结点

通过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!

你可能感兴趣的:(java,开发语言)