ReentrantLock源码解析——虽众但写
在看这篇文章时,笔者默认你已经看过AQS或者已经初步的了解AQS的内部过程。
先简单介绍一下ReentantLock
,跟synchronized
相同,是可重入的重量级锁。但是其用法则相当不同,首先ReentrantLock
要显式的调用lock方法表示接下来的这段代码已经被当前线程锁住,其他线程需要执行时需要拿到这个锁才能执行,而当前线程在执行完之后要显式的释放锁,固定格式
lock.lock();
try {
doSomething();
} finally {
lock.unlock();
}
1.ReentrantLock的demo程序
来通过下面这段代码简单的了解ReentrantLock
是如何使用的
// 定义一个锁
private static Lock lock = new ReentrantLock();
/**
* ReentrantLock的使用例子,并且验证其一些特性
* @param args 入参
* @throws Exception 错误
*/
public static void main(String[] args) throws Exception {
// 线程池
ThreadPoolExecutor executor = ThreadPoolUtil.getInstance();
executor.execute(() -> {
System.err.println("线程1尝试获取lock锁...");
lock.lock();
try {
System.err.println("线程1拿到锁并进入try,准备执行testForLock方法");
// 调用下方的方法,验证lock的可重入性
testForLock();
TimeUnit.MILLISECONDS.sleep(500);
System.err.println("线程1try模块全部执行完毕,准备释放lock锁");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.err.println("线程1释放lock锁,线程1释放锁2次,此时才算真正释放,验证了ReentrantLock加锁多少次就要释放多少次锁");
}
});
// 先睡他100ms,保证线程1先拿到锁
TimeUnit.MILLISECONDS.sleep(100);
executor.execute(() -> {
System.err.println("线程2尝试获取lock锁...");
lock.lock();
try {
System.err.println("线程2拿到锁并进入try");
} finally {
lock.unlock();
System.err.println("线程2执行完毕,释放lock锁");
}
});
}
/**
* 验证ReentrantLock具有可重入
*/
public static void testForLock() throws InterruptedException {
System.err.println("线程1开始执行testForLock方法,正准备获取lock锁...");
lock.lock();
try {
System.err.println("testForLock成功获取lock锁,证明了ReentrantLock具有可重入性");
TimeUnit.MILLISECONDS.sleep(200);
} finally {
lock.unlock();
System.err.println("testForLock释放lock锁,线程1释放锁一次");
}
}
结果图:
从结果图中,我们得到了很多信息,比如ReentrantLock
具备可重入性(testForLock
方法得出),并且其释放锁的次数必须跟加锁的次数保持一致(这样才能保证正确性);此外ReentrantLock
为悲观锁,在某个线程获取到锁之后其他线程在其完全释放之前不得获取(线程2充分证明了这一点,其开始获取锁的时间要比线程1的执行时间快许多,但还是被阻塞住了)。
2.获取锁的方法——lock()
okay,那来看下其内部是如何实现的,直接点击lock()
方法
public void lock() {
sync.lock();
}
看到其直接调用了sync
的lock()
方法,再点击进入
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
abstract void lock();
// ...
}
可以看到Sync
类是ReentrantLock
的一个内部类,继承了AQS
框架,也就是说ReentrantLock
就是AQS框架下的一个产物,那么问题就变得简单起来了。如果还没了解过AQS
的可以看下我另一篇文章——AQS框架详解,看过之后再回头看ReentrantLock
,你会发现,就这?
扯回来ReentrantLock
,这边可以看到内部类Sync
是一个抽象类,lock()
方法也是一个抽象方法,也就意味着这个lock
会根据子类的不同实现执行不同操作,点开子类发现有两个——公平锁和非公平锁。
里边的具体实现先放一放,回到ReentrantLock
的lock
方法
public void lock() {
sync.lock();
}
直接调用说明sync
已经被初始化过,那么在哪里进行初始化的呢?仔细翻一翻可以从ReentrantLock
的两个构造方法中发现猫腻
/**
* 构造方法1
* 无参构造方法,直接将sync初始化为非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 构造方法2
* 带参构造方法,根据传进来的布尔值决定将sync初始化为公平还是非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
这里顺带说一下,在AQS
有一个同步队列(CLH
),是一种先进先出队列。公平锁的意思就是严格按照这个队列的顺序来获取锁,非公平锁的意思就是不一定按照这个队列的顺序来。
那现在知道sync
是在创建ReentrantLock
的时候就进行了初始化,我们就来看下公平和非公平锁各自做了什么吧。
2.1 非公平锁
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());
else
// 否则的话调用AQS的acquire方法乖乖入同步队列等待去吧
acquire(1);
}
// AQS暴露出来需要子类重写的方法
protected final boolean tryAcquire(int acquires) {
// 方法解释在下方
return nonfairTryAcquire(acquires);
}
}
// 非公平锁的tryAcquire方法,该方法是放在Sync抽象类中的,为了tryLock的时候使用
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 当前锁的状态
int c = getState();
// 如果是0则表示锁是开放状态,可以争夺
if (c == 0) {
// 使用CAS设置为对应的值,在ReentrantLock中acquires的值一直是1
if (compareAndSetState(0, acquires)) {
// 成功了设置持有线程
setExclusiveOwnerThread(current);
return true;
}
}
/*
* 如果当前线程是持有线程,那么state的值+1
* 这里也是ReentrantLock可重入的原理
*/
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;
}
非公平锁基本的流程解释在上方的代码中已经在注释写出,相信不难看懂。不过有个需要注意的点要说一下,首先要看清楚非公平锁的定义,它是不一定按照队列顺序来获取,不是不按照队列顺序获取。
从上面的代码我们也可以看出来,非公平锁调用lock()
方法的时候会先调用一次CAS
来获取锁,成功了直接返回,这第一次操作没有按照队列的顺序来,但也只有这一次。如果失败了,入队之后还是乖乖的得按照CLH同步队列的顺序来拿锁,这一点要搞清楚。
2.3 公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// lock方法直接调用AQS的acquire方法,连一点争取的欲望都没有
final void lock() {
acquire(1);
}
// 公平锁的获取资源方法,该方法是在acquire方法类调用的
protected final boolean tryAcquire(int acquires) {
// 整体逻辑还是挺简单的,跟非公平有些类似
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/*
* c==0表示当前锁没有被获取
* 如果没有前驱节点或者前驱节点是头结点,
* 那么使用CAS尝试获取资源
* 成功了设置持有线程并返回true,失败了直接返回
*/
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;
}
}
公平锁的逻辑相对来说十分简单,lock
方法老老实实的去排队获取锁,而获取资源方法的逻辑也在代码注释写得很清楚了,没有什么需要多讲的。
3.锁释放
上面的理解之后释放锁的逻辑就简单的多了,直接放代码吧:
/*
* 解锁方法直接调用AQS的release方法
* 而release方法的去向又是跟tryRelease的返回值直接相关
* tryRelease方法的实现在内部类Sync中,具体在下方
*/
public void unlock() {
sync.release(1);
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// ...
// 释放资源的方法
protected final boolean tryRelease(int 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;
}
// ...
}
释放锁的逻辑在注释中解释得很清楚了,看完也知道由于ReentrantLock
是可重入的,所以锁的数值会逐渐增加,那么在释放的时候也要一个一个逐一释放。
主要的逻辑还是AQS
的release
方法中,这里详讲的话篇幅太多,有兴趣的话可以单独看下AQS
的文章,传送门:AQS。
4.ReentrantLock的可选择性
来讲下ReentrantLock
跟Synchonized
的一大不同点之一——Condition
。那么condition
是什么呢,简单来说就是将等待获取资源的线程独立出来分队,什么意思呢?举个例子,现在有8个线程同时争取一个锁,我觉得太多了,就把这个8个线程平均分成4队,等我觉得哪队OK就将那一队的线程叫出来争取这个锁。在这里的condition
就是队伍,4队就是4个condition
。
另外说一句,condition
(队伍)中的线程是不参与锁的竞争的,如果上方的8个线程我只将2个线程放入一个队,其他线程不建立队伍,那么其他线程会参与锁的竞争,而独立到队伍中的2个线程则不会,因为其被放在AQS
的等待队列中,等待队列是不参与资源的竞争的,我在另一篇文章——AQS框架详解写得很清楚了。还是那句话,AQS
懂了再看ReentrantLock
,理解难度就会低得多得多得多得多....
okay,那来简单看下Condition
如何使用
// 线程池
ThreadPoolExecutor executor = ThreadPoolUtil.getInstance();
// 这里只建了一个condition起理解作用,自己有兴趣的话可以多建几个模拟多点场景
Condition condition = lock.newCondition();
executor.execute(() -> {
System.err.println("线程1尝试获取lock锁...");
lock.lock();
try {
System.err.println("线程1拿到锁并进入try");
System.err.println("线程1准备进行condition操作");
/*
* 将当前线程即线程1放入指定的这个condition中,
* 如果是其他condition则调用其他condition的await()方法
*/
condition.await();
System.err.println("线程1结束condition操作");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.err.println("线程1执行完毕,释放lock锁");
}
});
// 保证线程1获取锁并且执行完毕
TimeUnit.MILLISECONDS.sleep(200);
executor.execute(() -> {
System.err.println("线程2尝试获取lock锁...");
lock.lock();
try {
System.err.println("线程2拿到锁并进入try");
// 唤醒condition的所有线程
condition.signalAll();
System.err.println("线程2将condition中的线程唤醒");
} finally {
lock.unlock();
System.err.println("线程2执行完毕,释放lock锁");
}
});
结果图:
可以从结果图中看到,
当线程调用了condition.await()
的时候就被放入了condition
中,并且此时将持有的锁释放,将自己挂起睡觉等待其他线程唤醒。所以线程2才能在线程1没执行完的情况获取到了锁,并且线程2执行完操作之后将线程1唤醒,线程1此时其实是重新进入同步队列(队尾)争取资源的,如果队列前方还有线程在等待的话它是不会拿到的,要按照队列顺序获取,可以自己在本地创多几个线程试一下。
通过这段简单的代码之后明显可以看到condition
具有不错的灵活性,也就是说提供了更多了选择性,这也就是跟synchronized
不同的地方,如果使用synchronized
加锁,那么Object
的唤醒方法只能唤醒全部,或者其中的一个,但是ReentrantLock
不同,有了condition
的帮助,可以不同的线程进行不同的分组,然后有选择的唤醒其中的一组或者其中一组的随机一个。
5.总结
ReentrantLock
的源码如果有了AQS
的基础,那么看起来是不费吹灰之力(开个玩笑,还是要比吹灰费劲的)。所以本章的篇幅也比较简单,先从一个例子说明了ReentrantLock
的用法, 并且通过这个例子介绍了ReentrantLock
可重入、悲观锁的几个特性;接着对其lock
方法进行源码跟踪,从而了解到其内部的方法都是由继承AQS
的内部类Sync
来实现的,而Sync
又分成了两个类,代表两种不同的锁——公平锁和非公平锁;接下来再讲到两种锁的具体实现和释放的逻辑,到这里加锁解锁的流程就完整了;最后再介绍ReentrantLock
的另一种特性——Condition
,这种特性允许其选择特定的线程来争夺锁,也可以选择性的唤醒锁,到这里整篇文章就告一段落。
孤独的人不一定是天才,还可能是得了郁抑症。
ReentrantLock源码解析——虽众但写的更多相关文章
- 死磕 java同步系列之ReentrantLock源码解析(二)——条件锁
问题 (1)条件锁是什么? (2)条件锁适用于什么场景? (3)条件锁的await()是在其它线程signal()的时候唤醒的吗? 简介 条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等 ...
- 深入浅出ReentrantLock源码解析
ReentrantLock不但是可重入锁,而且还是公平或非公平锁,在工作中会经常使用到,将自己对这两种锁的理解记录下来,希望对大家有帮助. 前提条件 在理解ReentrantLock时需要具备一些基本 ...
- 第六章 ReentrantLock源码解析2--释放锁unlock()
最常用的方式: int a = 12; //注意:通常情况下,这个会设置成一个类变量,比如说Segement中的段锁与copyOnWriteArrayList中的全局锁 final Reentrant ...
- ReentrantLock源码解析
ReentrantLock 1 数据结构 从上图可以看出,ReentrantLock的功能都是通过sync这个对象提供的. public class ReentrantLock implements ...
- Java并发之ReentrantLock源码解析(二)
在了解如何加锁时候,我们再来了解如何解锁.可重入互斥锁ReentrantLock的解锁方法unlock()并不区分是公平锁还是非公平锁,Sync类并没有实现release(int arg)方法,这里会 ...
- Java并发之ReentrantLock源码解析(四)
Condition 在上一章中,我们大概了解了Condition的使用,下面我们来看看Condition再juc的实现.juc下Condition本质上是一个接口,它只定义了这个接口的使用方式,具体的 ...
- 死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁
问题 (1)重入锁是什么? (2)ReentrantLock如何实现重入锁? (3)ReentrantLock为什么默认是非公平模式? (4)ReentrantLock除了可重入还有哪些特性? 简介 ...
- Java并发之ReentrantLock源码解析(三)
ReentrantLock和BlockingQueue 首先,看到这个标题,不要怀疑自己进错文章,也不要怀疑笔者写错,哈哈.本章笔者会从BlockingQueue(阻塞队列)的角度,看看juc包下的阻 ...
- 第五章 ReentrantLock源码解析1--获得非公平锁与公平锁lock()
最常用的方式: int a = 12; //注意:通常情况下,这个会设置成一个类变量,比如说Segement中的段锁与copyOnWriteArrayList中的全局锁 final Reentrant ...
随机推荐
- 神州优车挂牌新三板!专车B2C对决C2C将愈发狂暴?
近日,全国中小企业股份转让系统公告显示,神州优车已获准在新三板挂牌.神州优车作为神州专车的主营主体,此次挂牌新三板意味着神舟专车成功突围,成为"专车第一股".相比滴滴.Uber中国 ...
- 关于C++类中的三兄弟(pretect、private、public)
1.public修饰的成员变量 在程序的任何地方都可以被访问,就是公共变量的意思,不需要通过成员函数就可以由类的实例直接访问 2.private修饰的成员变量 只有类内可直接访问,私有的,类的实例要通 ...
- C++走向远洋——22(项目一,三角形,类)
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:sanjiaoxing.cpp * 作者:常轩 * 微信公众号: ...
- Samtec与Neoconix达成合作并和II-VI推出新产品
序言:Samtec近日动作不断, 近日Samtec与Neoconix达成合作并和II-VI推出新产品,以下是详细内容. Samtec与Neoconix签订Neoconix PCBeam 技术授权协议, ...
- python 软件目录规范
软件目录结构规范 软件开发规范 一.为什么要设计好目录结构? 1.可读性高: 不熟悉这个项目的代码的人,一眼就能看懂目录结构,知道程序启动脚本是哪个,测试目录在哪儿,配置文件在哪儿等等.从而非常快 ...
- 前端面试题-<!DOCTYPE>
现在的各种前端开发工具都足够强大,支持插入模板代码,也就导致我们往往会忽略已经自动生成的代码,而代码的第一行 DOCTYPE 声明,就是最容易忽略的部分. 一.DOCTYPE DOCTYPE 是 do ...
- php判断二个数最大公约数
$m = isset($_GET['m']) ? $_GET['m'] : 12; $n = isset($_GET['n']) ? $_GET['n'] : 8; //判断mn的大小 if($m&g ...
- R|生存分析 - KM曲线 ,值得拥有姓名和颜值
本文首发于“生信补给站”:https://mp.weixin.qq.com/s/lpkWwrLNtkLH8QA75X5STw 生存分析作为分析疾病/癌症预后的出镜频率超高的分析手段,而其结果展示的KM ...
- 高可用Keepalived+LVS搭建流程
本流程搭建1个master,1个backup节点的Keepalived,使用lvs轮询2个节点的服务. 一.使用版本 CentOS 7.7 Keepalived 1.3.5 ipvsadm 1.27( ...
- plsql乱码问题
1,问题:在plsql 中执行sql语句,查询结果带有中文,出现乱码,即" ??? ":如下: 2,解决: 1)输入sql语句 select * from V$NLS_PARAME ...