Lock框架为java并发编程提供了除synchronized之外的另外一种选择。synchronized是隐式实现,底层封装了对锁资源的获取和释放的所有实现细节,程序员不需要关心也没有办法关心这些细节,使用起来非常方便也非常安全。
而Lock由java语言实现,公开了锁资源获取和释放的所有细节,在资源锁定过程中提供了更多选项,在获取锁资源后,可以通过Condition对象对锁资源做细粒度的管理。
最关键的是Lock大量使用了CAS,充分利用“持有锁的线程不会长时间占用锁”这一假设,有可能的情况下就尽量先自旋、后锁定资源。所以多线程环境下Lock应该比synchronized有更好的性能。
java线程池框架Executor中大量使用了基于Lock接口的ReentrantLock,掌握ReentrantLock是深入理解各种Executor(ThreadPoolExecutor、ScheduledThreadPoolExecutor等)以及各种阻塞队列的必要前提。
Lock有独占锁、共享锁的区别,独占锁是指某一线程获取锁资源后即独占该锁资源、其他线程只能等待,共享锁是指多个线程能同时获得锁资源。
今天我们的研究对象是ReentrantLock,ReentrantLock是独占锁,主要研究内容:
#eentrantLock的基本概念
顾名思义,ReentrantLock是“可重入锁”,意思是同一线程可以多次获得锁,n次获得需要n次释放才能最终释放掉ReentrantLock。
ReentrantLock的基本原理:
以上是没有Condition对象参与的ReentrantLock的获取、释放锁资源的逻辑,相对比较简单。
有Condition参与的时候,情况会稍微复杂一点:
Condition举例:take方法中队列空的话,挂起等待
Condition举例:offer方法中写入队列后,唤醒等待的线程
对ReentrantLock应该有一个基本的认识了,如果只是想要对ReentrantLock做一个基本了解、能够看懂ReentrantLock的应用、而不是要从源码角度做深入研究的话,个人认为掌握上面这些基本原理应该就够了,保证能看懂阻塞队列、线程池中的有关ReentrantLock的源码逻辑了。
但是如果想要彻底搞清楚ReentrantLock到底是怎么实现以上逻辑的,就需要从源码角度继续做深入研究了。
多个线程同时竞争ReentrantLock锁资源的时候,只能有一个竞争获胜的线程获得锁资源、其他线程就只能排队等待。这个用来排队的队列就是CLH队列,AQS(AbstractQueuedSynchronizer)是实现CLH队列的虚拟类。
ReentrantLock有一个非常重要的属性Sync,Sync是AQS的虚拟扩展类,Sync有两个实现类:NonfairSync和FairSync,类结构如下:
NonfairSync和FairSync都是AQS的最终实现,AQS虚拟类是一个标准模板,定义了Lock锁的基本数据结构(阻塞队列)、并实现了Lock的绝大部分功能。
进入队列排队的线程被封装为Node,Node是AQS定义的内部类,是我们学习AQS首先要掌握的内容。
waitStatus: 等待状态,Node就是用来排队的,waitStatus就代表当前节点的等待状态,有以下几种等待状态:
prev :上一节点
next:双向队列嘛,当然也要有下一节点
Thread thread:节点的主角,排队线程
nextWaiter:Condition队列专用,用来指向Condition队列的下一节点
AQS的同步队列(CLH)以及Condition队列的节点都是用这个Node,所以Node类做了一部分针对两者的兼容设计,比如nextWaiter是针对Condtion队列的下一节点,next是针对CLH的下一节点。
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提供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!