java并发之彻底搞懂ReentrantLock

22 篇文章 2 订阅
5 篇文章 0 订阅

ReentrantLock

在Java5之前synchronized是仅有的同步手段,从Java5开始便提供了ReentrantLock,即再入锁的实现。
它的语义和synchronized基本相同,通过代码直接调用lock方法获取,代码编写也更加灵活。与此同时,位于java.util.concurrent.locks包下的ReentrantLock提供了很多使用方法,java.util.concurrent这个package即业界有名的J.U.C包,里面比较有名的工具类几乎都是基于Doug Lea大神写的AQS抽象类框架衍生出来的应用。

此外它与CountDownLatch、FutureTask、Semaphore一样基于AQS实现。

咱们可以看看ReentrantLock的源码:
在这里插入图片描述
我们可以看到这里有个lock方法里面调用了acquire方法,我们点进去看看:
在这里插入图片描述
看到里面有一个acquireQueued方法,再点进去:
在这里插入图片描述

我们可以看到,这个acquireQueued就是在AbstractQueuedSynchronizer这个抽象类里面,而AbstractQueuedSynchronizer即同步器,它是Java并发用来构建锁或其他同步组件的基础框架,是J.U.C package的核心。

例如:
在这里插入图片描述

一般使用AQS的方式,是继承AbstractQueuedSynchronizer来实现其抽象方法来管理同步状态,其他的像CountDownLatch、FutureTask、Semaphore都会有一个内部类,是AbstractQueuedSynchronizer这个抽象类的子类,那从原理上看,一种同步结构往往是可以利用其他的结构去实现的,但是对某种同步结构的倾向对导致复杂晦涩的实现逻辑,所以Doug Lea大神将基础的同步相关的常用操作抽象在AbstractQueuedSynchronizer当中了,那利用AQS为我们构建同步结构提供范本。

AQS的数据结构可以拆分为,volatile修饰的同步状态 state
在这里插入图片描述
还有getState和setState方法
在这里插入图片描述
以及一个先入先出即FIFO的等待线程队列
在这里插入图片描述
这些都是AQS的核心,同时它还有各种基于CAS的基础操作方法
在这里插入图片描述
以及各种期望具体同步结构去实现的acquire或者release方法
在这里插入图片描述

另外AQS要想实现一个同步结构,至少实现两个基本类型的方法,分别是acquire操作,它是用来获取资源的独占权;还有release操作,是用来释放对某个资源的独占。

AQS基础框架比较复杂,大约有两千多行的代码,希望大家有空多了解一下。

此外,ReentrantLcok能够实现很多synchronized无法做到的细节控制,比如可以控制fairness也就是公平性,这个我们待会探讨。但是编码中要注意,必须要调用unlock释放锁,不然当前线程就一直持有该锁而不去释放。
还有,ReentrantLock与synchronized的性能不能一概而论,其实synchronized经过后续版本的改进,表现可能会优于ReentrantLock。
ReentrantLock与一样是可重入的。

ReentrantLock公平性设置

我们刚刚提到,ReentrantLock可以控制公平性fairness,我们可以在创建该锁的时候选择是否是公平锁,像这样:

ReentrantLock fairLock = new ReentrantLock(true);

这里所谓的公平指的是,在竞争场景中,当公平性为true的时候会倾向于将锁赋予等待时间最久的线程,公平性是减少线程饥饿情况发生的办法,所谓的饥饿指的是个别线程长期等待锁但却始终无法获取锁的情况。

公平锁指的就是每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁,类似于排队打饭。
而所谓非公平锁指的就是每个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的先后顺序无关。

如果使用synchronized,我们无法实现对公平性的选择,其实通常场景中,公平性未必有想象中的那么重要,java默认的调度策略很少会导致饥饿情况的发生,与此同时,要想保证公平性,则会引入额外的开销,自然会导致一定的吞吐量下级,只有大家在程序中确实有公平性的必要,我们才会选择它。

ReentrantLock优点

ReentrantLock相比于synchronized,因为其可以像普通对象一样使用,所以可以来利用它提供各种便利的方法,进行精细的同步操作。
甚至可以实现synchronized难以表达的用例:

  • 判断是否有线程,或者某个特定线程,在排队等待获取锁
  • 带超时的获取锁的尝试
  • 感知有没有成功获取到锁

如果说ReentrantLock将synchronized转变为了可控的对象,那么是不是也能把wait、notify、notifyAll等操作转变为对应的对象呢,即能否把晦涩复杂的同步操作转变为直观可控的行为?
答案是肯定的,同样位于java.util.concurrent.locks里面的Condition做到了这一点。

Condition最典型的应用场景就是标准类库中的ArrayBlockingQueue。

ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列,线程安全是指ArrayBlockingQueue内部通过互斥锁保护进程资源,其互斥锁是通过ReentrantLock来实现的,实现了多线程对竞争资源的互斥访问,而有界则指的是ArrayBlockingQueue对应的数组是有界限的,阻塞队列是指多线程访问竞争资源时,当竞争资源已被某线程获取时,其他要获取该资源的线程需要阻塞等待。
ArrayBlockingQueue与Condition是组合的关系,ArrayBlockingQueue中包含了两个Condition对象:
在这里插入图片描述
一个是notEmpty,另一个是notFull。

而且Condition又依赖于ArrayBlockingQueue而存在,通过Condition可以实现对ArrayBlockingQueue更精确的访问。
我们来看一下ArrayBlockingQueue的构造函数:
在这里插入图片描述
我们可以看到,有两个成员变量notEmpty与notFull,它们都是由同一个lock创建出来的,而用在特定的操作中,而这里的lock就是ReentrantLock了,咱们再看看notEmpty用在哪里:
在这里插入图片描述
它首先是用在了take这个方法里,它的判断条件就是count == 0,也就是满足当队列为空,当队列为空时,试图take的线程的正确行为行该是等待有新的消息被加入到队列,最终并返回,即如果有消息则返回,没有消息就会等待。
由此可见,通过notEmpty的await就可以优雅的使用线程的等待逻辑。

咱们再来看看消息入队的方法:
在这里插入图片描述
我们可以看到,一旦有消息被放入队列当中count就进行加一的操作,那此时notEmpty就会调用signal方法通知等待的线程。signal就和notify是一样的。此时非空条件就会满足,进而take方法就能取到东西了。

通过signal和await的组合,ArrayBlockingQueue就能优雅的完成了条件判断和通知等待线程,非常顺畅就完成了状态流转。

我们注意到,notEmpty是通过lock的newCondition方法获取的。点击进去:
在这里插入图片描述
我们又会发现它是通过sync的newCondition获取的。
即来自于 ConditionObject这个对象。
在这里插入图片描述而ConditionObject是来自于AQS框架的。
在这里插入图片描述

总结

我们简单总结一下synchronized和ReentrantLock的区别

  • synchronized是关键字,ReentrantLock是类
  • ReentrantLock可以对获取锁的等待时间进行设置,避免死锁。
  • ReentrantLock可以获取各种锁的信息
  • ReentrantLock可以灵活地实现多路通知

最关键的是二者的锁机制实现也是不一样的,synchronized操作的是对象头中的Mark Word,而ReentrantLock底层是调用的Unsafe类的park方法加锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值