ReentrantLock源码学习总结 (一)
ReentrantLock 示例
private ReentrantLock lock = new ReentrantLock(true);
public void f(){
try {
lock.lock();
//do something
}
finally {
lock.unlock();
}
}
源码解析(公平锁-lock流程)
构造方法
//默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//构造参数传入是否使用公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
核心变量
private final Sync sync;
//大名鼎鼎的 AQS
abstract static class Sync extends AbstractQueuedSynchronizer{...}
//队列(链表)头
private transient volatile Node head;
//队列(链表)尾
private transient volatile Node tail;
//状态 state = 0 未加锁 > 0 已经加锁
private volatile int state;
ReentrantLock#lock()
public void lock() {
sync.lock();
}
FairSync#lock()
final void lock() {
acquire(1);
}
AbstractQueuedSynchronizer#acquire()
^: acquire v.(通过努力、能力、行为表现) 获得; 购得; 获得; 得到;
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
//第一步:尝试获取锁,如果获取成功,直接返回
//第二步:加入等待队列
//第三步:再次尝试获取锁
//if(!tryAcquire(arg)){
//加入等待队列
//Node node = addWaiter(Node.EXCLUSIVE);
//入队之后,再次尝试获取锁,在做一次努力,因为有可能此时上一个线程已经释放锁了,获取锁之后会返回是否被打断,如果被打断了,执行 selfInterrupt();
//if(acquireQueued(node,arg)){
//打断
//selfInterrupt();
//}
}
}
FairSync#tryAcquire(arg)
AQS 中并没有实现 tryAcquire
方法,交给了子类实现。
^: recursive 递归的;循环的
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
// acquires = 1
// 获取当前线程
final Thread current = Thread.currentThread();
//先获取加锁状态
int c = getState();
//状态为0,代表没有上锁
if (c == 0) {
//存在并发,重新判断是否直接进行CAS上锁
//hasQueuedPredecessors() 判断当前线程之前是否还有线程在排队等待锁,如果没有,就执行CAS修改state状态,如果修改成功,将当前线程变量赋值
if (!hasQueuedPredecessors() &&
//CAS 0 -> 1
compareAndSetState(0, acquires)) {
//加锁成功,给变量 exclusiveOwnerThread 赋值
setExclusiveOwnerThread(current);
return true;
}
}
//此时已经有线程占有锁,先判断,是否是自己占有锁,如果是自己,那就将 state + 1 实现可重入锁的特性
else if (current == getExclusiveOwnerThread()) {
//是自己占有的,将 state + 1
int nextc = c + acquires;
//int值溢出-一般场景中不会加这么多层
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//更新状态 state
setState(nextc);
return true;
}
// CAS 竞争失败,或者锁已经被其他线程占用,返回 false ,加锁失败
return false;
}
ReentrantLock#hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
//头结点
Node t = tail;
//尾节点
Node h = head;
Node s;
//第一种情况:就一个线程进入,此时 head 和 tail 都为 null,h!=t 不成立,直接返回 false,表示并没有任何线程正在队列中等待
//第二种情况:头部和尾部不一致 s= h.next == null ,按理说如果 头部和尾部不一致,那不会出现 h.next == null 的情况,但是在并发中,是会出现的,所以,说明此时正在有其他线程尝试获取锁,或者正在获取的路上,那么当前线程放弃获取,等其他线程去获取吧
//第三种情况:头结点的下一个节点不为 null ,但是 节点线程不是当前线程,说明前边还有一个线程在等待,当前线程还是老老实实的排队吧,获取锁失败。
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
单纯的看注释肯定也是有点懵逼的,这段代码要结合后续的代码去分析。下面我将结合一个队列模型图来继续分析后续的代码:
场景模拟
场景1:第一个线程 T1 尝试获取锁,此时队列中并没有任何(Node),h!=t
条件不成立,可以去获取锁了。
此时线程 T1获取锁成功,假如它瞬间就执行完了,释放锁,将state
设置为0。线程T2现在准备尝试获取锁了,因为T1已经将锁释放,所以T2会顺利获取锁。所以即使加了锁,在一些线程竞争较少的场景,锁不会影响程序的正常运行,可以忽略。
场景2:当然在高并发业务中,肯定没有这么简单,下面我们考虑线程 T1,T2同时竞争锁的情况,我们回到前面的代码:
FairSync#tryAcquire(arg)
int c = getState();
//状态为0,线程T1 ,T2 都进来了
if (c == 0) {
//此时T1 T2 存在竞争,CAS保证至少有一个能够获取锁,另外一个获取失败,那么假如T1获取成功了,T2获取失败了,此时要调用 addWaiter(Node.EXCLUSIVE) 方法,将T2加入到等待队列中
if (!hasQueuedPredecessors() &&
//CAS 0 -> 1
compareAndSetState(0, acquires)) {
//加锁成功,给变量 exclusiveOwnerThread 赋值
setExclusiveOwnerThread(current);
return true;
}
AbstractQueuedSynchronizer#addWaiter(Node mode)
//ReentrantLock中 mode = Node.EXCLUSIVE 独占锁
private Node addWaiter(Node mode) {
//新生成一个Node,mode 会赋值给nextWaiter(这个先忽略)
Node node = new Node(Thread.currentThread(), mode);
//找到尾部节点
Node pred = tail;
//如果尾部节点不为空,那么将此Node加入到尾部
if (pred != null) {
//将此节点的prev 改为 pred
node.prev = pred;
//CAS设置尾节点,如果成功,返回此节点,否则CAS失败,执行 enq 方法
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//enq 方法,自旋,保证节点肯定能够入队
enq(node);
return node;
}
从场景2中我们知道,此时 pred
是为 null 的,所以,这里直接走enq
方法
AbstractQueuedSynchronizer#enq(Node node)
private Node enq(final Node node) {
//自旋,必须将这个节点加入到队列中不可
for (;;) {
//第一次 tail 为null
//再次自旋之后,tail不为空
Node t = tail;
if (t == null) {
//设置头部,这里要注意,并不是直接把 T2 的Node 设置为头部,而是加入了一个新的 thread 为空的节点。用老师的话说就是,就好像买火车票排队一样,第一个人不属于排队,他已经在办理业务了,而从第二个人开始才算排队中,所以此时 head 节点为 new Node()
if (compareAndSetHead(new Node()))
//设置成功之后,进入下一次循环(此时队列见 图2)
tail = head;
} else {
//将尾节点赋值给 T2 所在的 Node
node.prev = t;
//CAS 设置尾节点,如果CAS 失败了,比如有其他线程抢先了,那么继续自旋,直到设置成功(此时队列见 图3)
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
图2:初始化队列
图3:T2加入到队列中
AbstractQueuedSynchronizer#acquireQueued(Node node, int arg)
boolean acquireQueued(final Node node, int arg) {
//失败标志
boolean failed = true;
try {
//是否被打断
boolean interrupted = false;
for (;;) {
//获取上一个节点
final Node p = node.predecessor();
//如果上一个节点为 Head 节点,就尝试获取锁,为什么是 Head 节点就尝试获取锁呢?因为上文我们分析了, Head 节点是不参与抢锁的,再次执行 tryAcquire 方法
if (p == head && tryAcquire(arg)) {
//如果抢到了锁,将此Node赋给Head
setHead(node);
//help GC,移除节点关系
p.next = null;
//获取锁成功
failed = false;
//返回结果
return interrupted;
}
//假如此时并没有获取到锁(场景2 中,T1还在执行,所以T2获取失败),此时要去验证一下,此节点是否需要执行 park,如果需要,就执行park,线程等待。(等唤醒之后,再次进入循环去尝试获取锁)
if (shouldParkAfterFailedAcquire(p, node) &&
/**
//阻塞当前线程,不要继续执行了,等待锁吧
LockSupport.park(this);
return Thread.interrupted();
*/
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果失败了,取消获取
if (failed)
cancelAcquire(node);
}
}
在进入下一个源码之前,我们先看一下 Node
的各个状态
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire(Node pred,Node node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//初始化状态为 0
int ws = pred.waitStatus;
//如果状态为SIGNAL,代表此线程可以被 park 了,第一次进来状态为0,再次循环之后,状态为SIGNAL,然后执行park操作 (上文代码:acquireQueued:parkAndCheckInterrupt())
if (ws == Node.SIGNAL)
return true;
//取消抢锁
if (ws > 0) {
do {
// PREV->PRED->NODE ====> PREV->NODE (移除pred)
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//将 状态设置为 SIGNAL ,从英文注释来看,就是当前状态为 0 或者 PROPAGATE ,(当前场景下 HEAD 状态为 0,CAS 设置状态为-1,注意,这里设置的是当前节点的前一个节点的状态,不是自己),设置完成之后,返回false,
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//继续循环(上文代码:acquireQueued: for(;;))
return false;
}
同样,如果T3此时也想获取锁,那么抱歉,加入队列,然后你的前一个节点也不是 Head 节点,直接 park
吧
链表中为什么没有 T1呢?它已经获取锁玩去了,不需要入队。
代码执行流程
总结
本文分析了 ReentrantLock
在使用公平锁下的lock
流程,用一个简单的场景去分析代码,在不同的情况下每段代码的注释是不一样的,所以高并发场景下的代码情况和分支真的非常多,也很复杂。有分析错误的地方欢迎大家指出。
需要关注的地方:
- 链表操作,设置 head ,tail 等
- head 不参与抢锁,thread 为 null
- 两个线程交替执行,并且很快释放锁的情况下,是不需要初始化队列的,即使初始化了队列,第二个线程还是会在入队之后再次尝试一次获取锁,实在获取不到,就 park。
- 第三个线程进来,直接排队,因为T2在前面
ReentrantLock源码学习总结 (一)的更多相关文章
- ReentrantLock源码学习总结 (二)
[^]: 以下源码分析基于JDK1.8 ReentrantLock 示例 private ReentrantLock lock = new ReentrantLock(true); public vo ...
- Java并发包源码学习系列:ReentrantLock可重入独占锁详解
目录 基本用法介绍 继承体系 构造方法 state状态表示 获取锁 void lock()方法 NonfairSync FairSync 公平与非公平策略的差异 void lockInterrupti ...
- 并发编程原理学习-reentrantlock源码分析
ReentrantLock基本概念 ReentrantLock是一个可重入锁,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,并且在获取锁时支持选择公平模式或者非公平模式 ...
- Java并发包源码学习之AQS框架(一)概述
AQS其实就是java.util.concurrent.locks.AbstractQueuedSynchronizer这个类. 阅读Java的并发包源码你会发现这个类是整个java.util.con ...
- 死磕 java同步系列之ReentrantLock源码解析(二)——条件锁
问题 (1)条件锁是什么? (2)条件锁适用于什么场景? (3)条件锁的await()是在其它线程signal()的时候唤醒的吗? 简介 条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等 ...
- ReentrantLock 源码分析以及 AQS (一)
前言 JDK1.5 之后发布了JUC(java.util.concurrent),用于解决多线程并发问题.AQS 是一个特别重要的同步框架,很多同步类都借助于 AQS 实现了对线程同步状态的管理. A ...
- Java并发包源码学习系列:AQS共享式与独占式获取与释放资源的区别
目录 Java并发包源码学习系列:AQS共享模式获取与释放资源 独占式获取资源 void acquire(int arg) boolean acquireQueued(Node, int) 独占式释放 ...
- Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析
目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...
- Java并发包源码学习系列:详解Condition条件队列、signal和await
目录 Condition接口 AQS条件变量的支持之ConditionObject内部类 回顾AQS中的Node void await() 添加到条件队列 Node addConditionWaite ...
随机推荐
- sed一些常用命令
[转] http://blog.chinaunix.net/uid-20754793-id-177657.html 下面是我学习sed时参照参考书总结的一些常用sed命令,基本上每条语句都进行了调试1 ...
- MPA JS CSS预处理方案
1.WebPack 添加配置文件webpack.config.js,直接在当前目录运行 webpack. var basepath = '/root/webapps/happ'; var glob = ...
- Mysql InnoDB引擎下 事务的隔离级别
mysql InnoDB 引擎下事物学习 建表user CREATE TABLE `user` ( `uid` bigint(20) unsigned NOT NULL AUTO_INCREMENT, ...
- Docker搭建disconf环境,三部曲之三:细说搭建过程
Docker下的disconf实战全文链接 <Docker搭建disconf环境,三部曲之一:极速搭建disconf>: <Docker搭建disconf环境,三部曲之二:本地快速构 ...
- 从零开始开发IM(即时通讯)服务端(二)
好消息:IM1.0.0版本已经上线啦,支持特性: 私聊发送文本/文件 已发送/已送达/已读回执 支持使用ldap登录 支持接入外部的登录认证系统 提供客户端jar包,方便客户端开发 github链接: ...
- ConcurrentHashMap 的工作原理及代码实现
ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组.Segment继承了ReentrantLock,所 ...
- 自荐RedisViewer一个有情怀的跨平台Redis可视化客户端工具
自荐一个有情怀的跨平台Redis可视化客户端工具--RedisViewer 转载自 最美分享Coder 2019-09-17 06:31:00 介绍 在以往的文章中曾经介绍过几款Redis的可视化工具 ...
- Fliptile(枚举+DFS)
Problem Description Farmer John knows that an intellectually satisfied cow is a happy cow who will g ...
- 算法与数据结构基础 - 图(Graph)
图基础 图(Graph)应用广泛,程序中可用邻接表和邻接矩阵表示图.依据不同维度,图可以分为有向图/无向图.有权图/无权图.连通图/非连通图.循环图/非循环图,有向图中的顶点具有入度/出度的概念. 面 ...
- POA理论:不要被你的目标欺骗了你
 最近通过<跃 ...