Java 并发问题(三)—— ReetrantLock 及其 Condition 实现原理

一、Lock接口

上一篇博文谈到的 synchronized 锁是隐式锁,即锁的持有和释放都是隐式的,我们无需干预,这篇我们主要讲解的是显式锁,即锁的持有和释放都必须由我们自己手动编写。在Java 1.5中,官方在 java.util.concurrent(简称为 JUC )并发包中加入了 Lock 接口,该接口中提供了加锁的 lock() 方法和 释放锁的 unLock() 方法对显式加锁和显式释放锁操作进行支持。线程使用 lock() 方法与 unlock() 对临界区进行包围,其他线程由于无法持有锁将无法进入临界区直到当前线程释放锁,注意 unlock() 操作必须在 finally 代码块中,这样可以确保即使临界区执行抛出异常,线程最终也能正常释放锁。

二、并发基础组件 AQS 与 ReetrantLock

1. AQS工作原理概要

AbstractQueuedSynchronizer 又称为队列同步器 (后面简称 AQS) ,它是用来构建锁或其他同步组件的基础框架,内部通过一个 int 类型的成员变量 state 来控制同步状态,当 state = 0 时,则说明没有任何线程占有共享资源的锁,当 state = 1 时,则说明有线程目前正在使用共享变量,其他线程必须加入“同步队列” (底层数据结构为双向链表,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作。类比 synchronized 的 MonitorObject 的_WaitSet)进行等待,AQS 内部通过内部类 Node 构成 FIFO 的同步队列来完成线程获取锁的排队工作,同时利用内部类 ConditionObject 构建等待队列(底层数据结构为单向链表,类比 synchronized 的 MonitorObject 的 _EntrySet),当 Condition 调用 await() 方法后,线程将会加入等待队列中,而当Condition调用 signal()/signalAll() 方法后,线程将从等待队列转移动同步队列中进行锁竞争。注意这里涉及到两种队列,一种的同步队列,当某线程获取对象锁时,因为该对象锁已被其他线程占有而等待,这个某线程将加入同步队列等待,而另一种则是等待队列 (可有多个),通过 Condition 调用 await() 方法释放锁后,将加入等待队列。关于 Condition 的等待队列我们后面再分析,这里我们先来看看AQS中的同步队列模型,如下

24213256_StEX.jpg

head和tail分别是AQS中的变量,其中head指向同步队列的头部,注意head为空结点,不存储信息。而tail则是同步队列的队尾,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作。state变量则是代表同步状态,执行当线程调用lock方法进行加锁后,如果此时state的值为0,则说明当前线程可以获取到锁(在本篇文章中,锁和同步状态代表同一个意思),同时将state设置为1,表示获取成功。如果state已为1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为Node结点加入同步队列等待。其中Node结点是对每一个访问同步代码的线程的封装,从图中的Node的数据结构也可看出,其包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。每个Node结点内部关联其前继结点prev和后继结点next,这样可以方便线程释放锁后快速唤醒下一个在等待的线程,Node是AQS的内部类。

其中 SHARED 和 EXCLUSIVE 常量分别代表共享模式和独占模式:

1. 共享模式是一个锁允许“多条线程”同时操作,如信号量Semaphore采用的就是基于AQS的共享模式实现的。

2. 独占模式则是同一个时间段只能有“一个线程”对共享资源进行操作,多余的请求线程需要排队等待,如ReentranLock。

变量 waitStatus 则表示当前被封装成 Node 结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。 

1. CANCELLED:waitStatus = 1 时,在同步队列中等待获取锁的线程等待超时(tryLock(time) 等待时间大于time的值)或者被中断 ( lockInterruptibly() 方法获取锁时),需要从同步队列中取消封装该线程的Node结点,结点的 waitStatus = 1 ,代表CANCELLED。即结束状态,进入该状态后的结点将不会再改变。

2. SIGNAL:waitStatus = -1时,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或者被取消,将会通知该后继结点的线程去获取锁,执行对象里的同步块或者方法。说白了,就是该结点处于唤醒状态,只要前继结点释放了锁,就会通知标识为SINGNAL的后继结点的线程获取锁,执行同步块。

3. CONDITION: waitStatus = -2时,于Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的 signal()/signalAll() 方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

4. PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。 

5. 0状态:值为0,代表初始化状态。 

pre和next,分别指向当前Node结点的前驱结点和后继结点,thread变量存储的请求锁的线程。nextWaiter,与Condition相关,代表等待队列中的后继结点,关于这点这里暂不深入,后续会有更详细的分析,嗯,到此我们对Node结点的数据结构也就比较清晰了。总之呢,AQS作为基础组件,对于锁的实现存在两种不同的模式,即共享模式(如Semaphore)和独占模式(如ReetrantLock),无论是共享模式还是独占模式的实现类,其内部都是基于AQS实现的,也都维持着一个虚拟的同步队列,当请求锁的线程超过现有模式的限制时,会将线程包装成Node结点并将线程当前必要的信息存储到node结点中,然后加入同步队列等会获取锁,而这系列操作都有AQS协助我们完成,这也是作为基础组件的原因,无论是Semaphore还是ReetrantLock,其内部绝大多数方法都是间接调用AQS完成的。

这里以ReentrantLock为例,简单讲解ReentrantLock与AQS的关系 

24213256_IQPK.jpg

1. AbstractOwnableSynchronizer:抽象类,定义了存储独占当前锁的线程和获取的方法

2. AbstractQueuedSynchronizer:抽象类,AQS框架核心类,其内部以虚拟队列的方式管理线程的锁获取与锁释放,其中获取锁(tryAcquire方法)和释放锁(tryRelease方法)并没有提供默认实现,需要子类重写这两个方法实现具体逻辑,目的是使开发人员可以自由定义获取锁以及释放锁的方式。

3. Node:AbstractQueuedSynchronizer 的内部类,用于构建虚拟队列(链表双向链表),管理需要获取锁的线程。

4. Sync:抽象类,是ReentrantLock的内部类,继承自AbstractQueuedSynchronizer,实现了释放锁的操作(tryRelease()方法),并提供了lock抽象方法,由其子类实现。

5. NonfairSync:是ReentrantLock的内部类,继承自Sync,非公平锁的实现类。

6. FairSync:是ReentrantLock的内部类,继承自Sync,公平锁的实现类。

7. ReentrantLock:实现了Lock接口的,其内部类有Sync、NonfairSync、FairSync,在创建时可以根据fair参数决定创建NonfairSync(默认非公平锁)还是FairSync。

ReentrantLock内部存在3个实现类,分别是Sync、NonfairSync、FairSync,其中Sync继承自AQS实现了解锁tryRelease()方法,而NonfairSync(非公平锁)、 FairSync(公平锁)则继承自Sync,实现了获取锁的tryAcquire()方法,ReentrantLock的所有方法调用都通过间接调用AQS和Sync类及其子类来完成的。从上述类图可以看出AQS是一个抽象类,但请注意其源码中并没一个抽象的方法,这是因为AQS只是作为一个基础组件,并不希望直接作为直接操作类对外输出,而更倾向于作为基础组件,为真正的实现类提供基础设施,如构建同步队列,控制同步状态等,事实上,从设计模式角度来看,AQS采用的模板模式的方式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作以及解锁操作,为什么这么做?这是因为AQS作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式与独占模式,而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的实现,所以提供了模板方法给子类使用,也就是说实现独占锁,如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法,这样做的好处是显而易见的,无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过是加锁解锁的逻辑不同罢了,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可,AQS提供给独占模式和共享模式的模板方法。

在了解AQS的原理概要后,下面我们就基于ReetrantLock进一步分析AQS的实现过程,这也是ReetrantLock的内部实现原理。

三、基于ReetrantLock分析AQS独占模式实现过程

1. ReetrantLock中非公平锁 

24213257_wNIb.jpg

注解:

1. 线程首次获取锁时,第一次,使用lock() 方法获取锁,内部使用CAS获取同步锁,如果失败,使用tryAcquire() 方法再次获取锁,内部实现也是CAS,这次获取锁如果也失败,则把该线程封装为Node,加入同步队列中。

 

2. ReetrantLock中公平锁 

ReetrantLock中公平锁的获取方法唯一的不同是在使用CAS设置尝试设置state值前,调用了hasQueuedPredecessors()判断同步队列是否存在结点,如果存在必须先执行完同步队列中结点的线程,当前线程放入同步队列的队尾进入等待状态。

这就是非公平锁与公平锁最大的区别,即公平锁在线程请求到来时先会判断同步队列是否存在结点,如果存在先执行同步队列中的结点线程,当前线程将封装成node加入同步队列等待。

而非公平锁呢,当线程请求到来时,不管同步队列是否存在线程结点,直接尝试获取同步状态,获取成功直接访问共享资源,但请注意在绝大多数情况下,非公平锁才是我们理想的选择,毕竟从效率上来说非公平锁总是胜于公平锁。 

关于synchronized 与ReentrantLock

在JDK 1.6之后,虚拟机对于synchronized关键字进行整体优化后,在性能上synchronized与ReentrantLock已没有明显差距,因此在使用选择上,需要根据场景而定,大部分情况下我们依然建议是synchronized关键字,原因之一是使用方便语义清晰,二是性能上虚拟机已为我们自动优化。而ReentrantLock提供了多样化的同步特性,如超时获取锁、可以被中断获取锁(synchronized的同步是不能中断的)、等待唤醒机制的多个条件变量(Condition)等,因此当我们确实需要使用到这些功能是,可以选择ReentrantLock 

三、神奇的Condition

关于Condition接口

在并发编程中,每个Java对象都存在一组监视器方法,如wait()、notify()以及notifyAll()方法,通过这些方法,我们可以实现线程间通信与协作(也称为等待唤醒机制),如生产者-消费者模式,而且这些方法必须配合着synchronized关键字使用,关于这点,如果想有更深入的理解,可观看博主另外一篇博文【 深入理解Java并发之synchronized实现原理】,与synchronized的等待唤醒机制相比Condition具有更多的灵活性以及精确性,这是因为notify()在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制,也就带来了更多灵活性了,我们可以简单理解为以下两点 通过Condition能够精细的控制多线程的休眠与唤醒。 对于一个锁,我们可以为多个线程间建立不同的Condition。

 

转载于:https://my.oschina.net/cughmy/blog/2208126

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值