【JDK1.8】JUC——ReentrantLock
一、前言
在之前的几篇中,我们回顾了锁框架中比较重要的几个类,他们为实现同步提供了基础支持,从现在开始到后面,就开始利用之前的几个类来进行各种锁的具体实现。今天来一起看下ReentrantLock,首先来看一下Java doc 上对ReentrantLock的解释:
ReentrantLock,作为可重入的互斥锁,具有与使用synchronized方法和语句相同的基本行为和语义,但功能更强大。
对上面这句话的解释:
- 拥有和synchronized关键字一样的行为,可重入互斥(注意,synchronized也是可重入的)
- 更强大的功能:比如支持公平锁和非公平锁,前面文章提到过的Condition的使用等。
另外来看一下它的最佳时间:
class X {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
// 上锁
lock.lock();
try {
// 执行方法体
} finally {
lock.unlock()
}
}
}
要点就是try-finally,在执行的最后,无论是否出错都调用unlock解锁,保证释放资源。
二、源码分析
2.1、成员变量
早在第一章JUC.Lock综述的时候,我们就大体看过juc包里的关系图,上面提到过,ReentrantLock支持公平锁和非公平锁,其原因就是内部实现了两个内部类FairSync
和NonfairSync
,分别实现了对应的支持,先来看一下成员变量:
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* fair = true:公平锁, false:非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
这里由于final关键字,方便理解,直接将构造方法也一并放了进来。
- 默认构造器在初始化的时候,实例化的是非公平锁,举个栗子, 我去买蛋糕,蛋糕店刚好出炉了一个蛋糕,我刚好碰到买到了,那我就买回去了。
- 而带fair的构造器,为true的时候,实例化的是公平锁,再举个栗子,我去买蛋糕,蛋糕店刚好又出炉了一个,我想买,但是人家已经预定好了,要买就得排队等。
2.2、内部类
前面提到的FairSync
和NonfairSync
都继承自ReentrantLock
的内部类,而在JUC关系图中,Sync
在大部分的锁框架中都各自进行了不同的实现,但是都继承自AQS。
2.2.1、Sync
一起来看一下ReentrantLock
中的Sync
实现:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
// 尝试获取非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// AQS中的state成员变量,0表示没有线程持有锁
int c = getState();
if (c == 0) {
// cas设置入锁次数,仅尝试一次,成功则设置当前线程为独占线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程为独占线程,则重入次数叠加
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 否则尝试获取锁失败
return false;
}
// 释放锁
protected final boolean tryRelease(int releases) {
// 由于可重入性,所以获取当前重入次数,与releases相减
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 为0则说明已经全部释放,则清空持有状态
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
...
}
lock()
的逻辑由继承的NonfairSync
和FairSync
自己实现。
这里笔者阅读的时候注意到一个问题:前面提到FairSync
是公平锁,每个线程按照队列的顺序来获取,但是其父类却有nonfairTryAcquire()
方法来尝试直接获取锁,这一实现放在NonfairSync
中不是更合适吗?为什么要放在父类中呢?
仔细查看代码后发现,ReentrantLock
里有tryLock()
方法:允许线程尝试获取一次锁,有则获得锁,返回true,没有则返回false。
那么就可以解释的通了,因为这个是ReentrantLock的public方法,所以不论是公平锁还是非公平锁,都可以调用,所以说,nonfairTryAcquire()
方法放在的父类Sync
当中。
2.2.2、NonfairSync
下面是非公平锁的实现:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// cas 原子性操作将state设置为1,如果设置成功,则说明当前没有线程持有锁
if (compareAndSetState(0, 1))
//把当前线程设置为独占锁
setExclusiveOwnerThread(Thread.currentThread());
// 反之则锁已经被占用,或者set失败
else
// 调用父类AQS分析里提到过的方法,以独占模式获取对象,acquire会调用tryAcquire
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
其实非公平锁核心实现在上一篇AQS之中就基本分析过了,所以这里的代码就相对简单。
但是为什么lock()
方法不直接调用acquire(1)
,而是直接先尝试CAS操作设置呢,笔者暂时没有想明白,因为调用acquire(1)
后,会进入tryAcquire(1)
,里面的操作其实是一样的,估计就是为了更快尝试获取?
2.2.3、FairSync
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平锁的体现就在这段代码里,就算没有线程占有锁,也会尝试判断队列里的线程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入的操作设置
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
与NonfairSync
不同,公平锁的tryAcquire
中,当发现当前没有线程持有锁的时候,会判断队列中有无前驱节点,之所以要判断的原因是:
在当前持有锁的线程调用unlock()
的的过程中,存在的这样一个过程:
在tryRelease()
到唤醒后继节点的过程中,可能有新的线程进来,这个时候,就需要判断队列是否有其他节点等待了,这就是公平锁的奥义吧。
详情查看hasQueuedPredecessors
代码,查看当前线程前有没有前驱节点:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
其中代码值得我们认真思考一下:
- 为什么要先从tail开始赋值?
- 说明时候h.next为null
这两点,我们要结合入队列时候的代码说起,在前面结束AQS的时候,分析过enq()
方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 队列初始化
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
// 重复执行插入直到return
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
我们先假设从head开始赋值:
当第一个线程调用enq的时候,cpu切换,进入了线程t2的hasQueuedPredecessors, 首先对head进行赋值,此时还没有到compareAndSetHead(new Node())
,那么此时 head = null,这个时候cpu切换,t1继续执行,执行完了tail == head
,再切换回t2,继续执行Node t = tail;
,这个时候,在return的时候,h != t
成立,当调用(s = h.next) == null
,h为null,报了空指针。
所以先从tail开始赋值,至少能保证在tail有值的时候,head必然有值!
另外什么时候h.next == null
,其实可以从enq
的else里找到答案,也是第一次enq插入空队列的时候,当线程执行到compareAndSetTail(t, node)
的时候,head != tail
,但是此时head.next
还未开始赋值,所以为null。
三、总结
关于ReentrantLock的使用例子,其实在第一篇将Lock的时候,就曾经有提到过,是Java Doc上提供的一个例子,典型的生产者-消费者模式,这里笔者就不赘述了。其实ReentrantLock关键的核心实现在于AQS,AQS仔细阅读的话,还是有很多值得推敲的地方,再一次觉得它的实现博大精深~最后谢谢各位园友观看,如果有描述不对的地方欢迎指正,与大家共同进步!
【JDK1.8】JUC——ReentrantLock的更多相关文章
- 【JDK1.8】JUC.Lock综述
一.前言 前段时间结束了jdk1.8集合框架的源码阅读,在过年的这段时间里,一直在准备JUC(java.util.concurrent)的源码阅读.平时接触的并发场景开发并不很多,但是有网络的地方,就 ...
- 【JDK1.8】JUC——AbstractQueuedSynchronizer
一.前言 在上一篇中,我们对LockSupport进行了阅读,因为它是实现我们今天要分析的AbstractQueuedSynchronizer(简称AQS)的基础,重新用一下最开始的图: 可以看到,在 ...
- 【JDK1.8】JUC——LockSupport
一.前言 Basic thread blocking primitives for creating locks and other synchronization classes. 用于创建锁定和其 ...
- 【JDK1.8】Java 8源码阅读汇总
一.前言 万丈高楼平地起,相信要想学好java,仅仅掌握基础的语法是远远不够的,从今天起,笔者将和园友们一起阅读jdk1.8的源码,并将阅读重点放在常见的诸如collection集合以及concu ...
- 【Java多线程】JUC包下的工具类CountDownLatch、CyclicBarrier和Semaphore
前言 JUC中为了满足在并发编程中不同的需求,提供了几个工具类供我们使用,分别是CountDownLatch.CyclicBarrier和Semaphore,其原理都是使用了AQS来实现,下面分别进行 ...
- 【JDK1.8】JDK1.8集合源码阅读——总章
一.前言 今天开始阅读jdk1.8的集合部分,平时在写项目的时候,用到的最多的部分可能就是Java的集合框架,通过阅读集合框架源码,了解其内部的数据结构实现,能够深入理解各个集合的性能特性,并且能够帮 ...
- 【JDK1.8】JDK1.8集合源码阅读——HashMap
一.前言 笔者之前看过一篇关于jdk1.8的HashMap源码分析,作者对里面的解读很到位,将代码里关键的地方都说了一遍,值得推荐.笔者也会顺着他的顺序来阅读一遍,除了基础的方法外,添加了其他补充内容 ...
- 【JDK1.8】JDK1.8集合源码阅读——TreeMap(一)
一.前言 在前面两篇随笔中,我们提到过,当HashMap的桶过大的时候,会自动将链表转化成红黑树结构,当时一笔带过,因为我们将留在本章中,针对TreeMap进行详细的了解. 二.TreeMap的继承关 ...
- 【Java并发】JUC—ReentrantReadWriteLock有坑,小心读锁!
好长一段时间前,某些场景需要JUC的读写锁,但在某个时刻内读写线程都报超时预警(长时间无响应),看起来像是锁竞争过程中出现死锁(我猜).经过排查项目并没有能造成死锁的可疑之处,因为业务代码并不复杂(仅 ...
随机推荐
- 自己根据js的兼容封装了一个小小的js库
var gys = function () { } //oParent父节点 //获取所有的子元素 gys.prototype.getElementChildren = function (oPare ...
- Apache Kudu as a More Flexible And Reliable Kafka-style Queue
Howdy friends! In this blog post, I show how Kudu, a new random-access datastore, can be made to fun ...
- Linux操作系统,服务器端的主流
1.无意之间,一直使用的Windows,其实也是在Unix的基础之上开发的,什么xp,win7等都是,就是Unix的一些堆砌,其实这样说也不对.但是windows的复杂程度比Linux要复杂,因为Wi ...
- json 拖拽
1.梳理知识点 1.事件对象 e || event 2.事件对象的属性 鼠标事件对象 : 坐标属性 : clientX clientY pageX pageY offset ...
- opencv查看源代码
这一节是一个插曲,有的人刚开始学opencv就看源代码,有的人直接拿着opencv的API用...... 学了一个多月opencv了,就是没找到源代码,想看的时候都是从网上找的,或者看网上说从哪个文件 ...
- Tomcat组成与工作原理
laosijikaichele 关注 0.2 2018.06.02 10:44 字数 5175 阅读 2798评论 0喜欢 8 原文:https://juejin.im/post/58eb5fdda ...
- 2013年6月编程语言排行榜,C语言位据第一位
2013年6月编程语言排行榜,C语言位据第一位 C语言是很多主流开发语言的母体,.NET的底层,Java的底层都是C开发的,虽然很多新语言来势汹汹,但究其根源,都脱离不开C语言
- 10. 数据模型(ER图)转为sql脚本,要求导出的字段都是大写的
1.进入Tools-->Execute Commands-->Edit/Run Script,执行以下脚本即可实现所有字段转为大写; Option Explicit ValidationM ...
- 1.获取服务器IP、端口等
比如,页面内部有一个连接,完整的路径应该是 http://192.168.0.1:8080/myblog/authen/login.do 其中http://server/是服务器的基本路径,myblo ...
- 获取ASPxGridView 中的数据(仅仅是获取;注意模板是如何获取的)
1.取得控件值 using System.Collections.Generic; //取得当前控件值的集合 直接寻找控件的ID List<object> keyValues = this ...