B9 Concurrent 重入锁(ReentrantLock)
【概述】
java.util.concurrent.locks.ReentrantLock 实现 java.util.concurrent.locks.Lock 接口,加锁(lock)和 解锁(unlock)方法都基于 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)实现,AQS 是基于 sun.misc.Unsafe 类的 CAS算法相关方法实现的。
【代码实例】
import java.util.concurrent.locks.ReentrantLock;
public class Main {
public static void main(String[] args) {
Command c = new Command();
int nThreads = 5;
for(int i = 0; i < nThreads; i++){
new Thread(c).start();
}
}
}
class Command implements Runnable {
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try{
lock.lock();
System.out.println(Thread.currentThread().getName() + ": 获得锁!");
}catch(Exception e){
//处理异常
}finally{
System.out.println(Thread.currentThread().getName() + ": 释放锁!");
lock.unlock();
}
}
}
打印结果: 一个线程独占锁(排他锁),释放锁后其他的线程才能获得锁。

【ReentrantLock 初始化】
1). 首先来看下 ReentrantLock 的属性定义:

ReentrantLock 的功能主要依靠定义的这个 Sync 类型的变量 sync 来实现。Sync 是 ReentrantLock 创建的一个静态内部类,它继承了 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer),即 ReentrantLock 的主要功能是依靠 AQS 实现的。
2). ReentrantLock 的构造器

ReentrantLock 提供了两种构造器,主要功能是对变量 sync 进行初始化,提供了 FairSync 和 NonFairSync两种实现,从命名可以看出分别对应公平锁和非公平锁的实现;其中 fair 为 true 时初始化为 FairSync(公平锁),fair 为 false 时初始化为 NonfairSync(非公平锁);无参构造器默认初始化为 NonfairSync。所谓 “公平” 在于是否按照线程申请锁的顺序获得锁,FairSync(公平锁)使用队列实现首先申请锁的先获得锁(FIFO),增加了额外的队列操作开销;相对而言,NonfairSync(非公平锁)的并发效能更高,吞吐量更高。
【AQS 初始化】
1). AQS 的实现依靠 sun.misc.Unsafe 类中 CAS 算法相关的方法,故这里初始化了 Unsafe 对象 unsafe,以及相关变量的内存偏移量(offset),用于后面的 CAS 操作。

2). AQS 属性介绍:AQS 使用双向链表的数据结构存储请求锁的线程,所以设计了一个节点类 Node,Node 类除了双向链表的前继指针(volatile Node prev)和后继指针(volatile Node next),存储数据包括 线程对象(volatile Thread thread)、等待状态(volatile int waitStatus)、下一个等待指针(Node nextWaiter)。一个节点表示一个线程。
下面是一个关于 waitStatus 的值说明:

AQS 除了双向链表的首节点(volatile Node head) 和 尾节点(volatile Node tail)外,还有一个状态属性(volatile int state)用于表示锁的状态。如果当前没有线程获得锁,state 的值为 0;若当前已经有一个线程获得锁,由于锁可重入,每获得一次锁加数加 1。
下面代码展示了重入锁的实现,打印了线程重新获得同一把锁时,state 的值变化。
import java.util.concurrent.locks.ReentrantLock;
public class Main{
public static void main(String[] args){
Command c = new Command();
int nThreads = 5;
for(int n = 0; n < nThreads; n++){
new Thread(c).start();
}
}
}
class Command implements Runnable{
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try{
lock.lock();
run2();
}catch(Exception e){
}finally{
lock.unlock();
}
}
public void run2(){
try{
lock.lock();
System.out.println(Thread.currentThread().getName() + ":" + lock.getHoldCount());
}catch(Exception e){
}finally{
lock.unlock();
}
}
}
打印结果:

- getHoldCount 方法返回了当前线程获得锁的数量,如果当前线程没有获得锁则返回 0。可以看到这里调用的是变量 sync 的 getHoldCount() 方法。

- 这里需要判断当前线程是否获得锁,即是否为独占锁的线程,是则调用 getState() 方法返回获得锁的数量,否则返回 0。


- AQS 继承了 java.util.concurrent.locks.AbstractOwnableSynchronizer,该类存储了独占锁的线程的值。


- AQS 中的 getState() 方法返回的就是 state 的值。

【加锁操作 lock()】
- lock() 的用途是当前线程尝试去获得锁,如果当前锁没有被其他线程持有,则当前线程可以立即获得锁,设置持有锁数量为 1;如果当前线程已经持有锁,再次调用 lock() 方法时会在原来持有锁数量上加 1;如果锁被其他线程持有,则当前线程保持休眠状态直到锁的持有数被设置为 1 的时候,才会被激活尝试去获得锁。


首先来看下,NonfairSync(非公平锁)的实现:

- compareAndSetState(0, 1) 是去获得锁,如果返回 true, 则获得锁成功,否则获得锁失败。这个方法是 AQS 中的方法,使用 Unsafe 类的 CAS 方法 compareAndSwapInt 尝试将 state 的值从 0 更新为 1。如果当前没有任何线程持有锁,state 为 0,则 CAS 成功,返回 true。注意 state 使用 volatile 修饰,state 的值被当前线程修改后,立即从工作内存刷新回主内存,其他线程可见最新的修改。

setExclusiveOwnerThread(Thread.currentThread()); 则是将当前线程设置独占锁的线程。
- 如果锁已经被持有(state != 0),持有线程可能是当前线程,也可能是其他线程。执行 acquire(1);

- tryAcquire 方式是锁已经被持有的情况下,尝试去获得锁。

获取锁的最新状态,通过是否为 0 判断锁是否被持有,如果没有被持有,跟上面一样的操作,通过 CAS 操作尝试去获得锁,如果获得锁成功则设置当前线程为独占锁的线程,返回 true;如果当前锁已被持有,判断独占锁的线程是否为当前线程,为当前线程则累加锁的数量,修改 state 变量,返回 true。例如一个线程持有锁的数量为 5,则 state 的值为 5。如果当前线程无法获得锁,则返回 false。

- 如果 tryAcquire 返回 false, 则添加到独占锁等待队列中(addWaiter(Node.EXCLUSIVE))

新建了一个 Node 节点 node,设置线程为当前线程,模式为独占锁(Node.EXCLUSIVE)
这里创建了一个 CHL 队列锁,队列中的元素通过自旋操作尝试去获得锁。在队列中的节点按照 FIFO 的规则,越先进入队列的节点对应的线程越先获得锁。



- 创建完 Node 节点 node 并加入 CHL 队列后,尝试通过队列获得锁。这里是一个自旋操作,判断当前节点 node 的前继节点(node.prev)是否为 head 节点,如果是,则使用 tryAcquire() 方法去尝试获得锁。

- 成功获得锁后,通过 setHead(node) 将 head 节点设置为 node,并将 node 节点的 prev(前继节点) 和 thread(存储线程)设置为 null。设置 failed 变量为 false,并将 interrupted(是否中断)返回。

- 每一次自旋操作,如果获取锁失败,则线程可以shouldParkAfterFailedAcquire 判断 pred (node 的前继节点)是否已经被阻塞。
Node.SIGNAL : 值为 -1,waitStatus(等待状态) 设置为这个值说明节点线程已经要求其他的线程去将它唤醒(LockSupport#unpark),所以可以安全地对它进行阻塞(LockSupport#park),返回 true,其他情况都返回 false。
Node.CANCEL:值为 1,唯一一个大于 0 的值,设置为这个值说明当前 pred (node 的前继节点)已经取消获得锁的操作,应该继续查找其前继节点(ws != Node.CANCEL)作为 node 的前继节点。
其他情况,会调用 compareAndSetWaitStatus 方法将 pred (node 的前继节点)的 waitStatus(等待状态)更新为 NODE.SIGNAL。

- 如果 shouldParkAfterFailedAcquire 返回 true, 即 node 的 前继节点已经被阻塞,则对当前线程进行 park 操作,并检查是否中断。这里使用 LockSupport#park() 方法进行阻塞线程。通过 Thread.interrupted() 判断线程是否被中断,并重置中断状态。如果线程被中断,则该方法返回 true, acquireQueued 方法中设置 interrupted 为 true。

- 线程被重置中断后,返回是否中断为 true, 故在 acquire 方法中执行了 selfInterrupt 进行线程中断。


- 最后在 finally 块中判断节点线程是否获得锁成功(failed = false),如果失败(failed = true),则执行 cancelAcquire 操作。(这里的代码不细说)
- 接下来看 FairSync(公平锁)如何实现 lock(加锁操作)
对比 NonFairSync(非公平锁),NonFairSync 会尝试先去获得锁,即后申请锁的线程有可能优先获得锁;而 FairSync(公平锁)则按照 FIFO 的原则让先申请锁的线程去获得锁。


- 可以看到 FairSync(公平锁)的 tryAcquire 方法中与 NonFairSync(非公平锁)方法不同的是,它会先去进行 hasQueuedPredecessors 方法判断当前队列是否有线程正在进行获得锁操作。若返回 true,则有其他线程正在进行获得锁操作,当前线程不再进行获得锁操作,tryAcquire 方法返回 false。

- 然后同样进入队列,执行与 NonfairSync(非公平锁)一样的操作

【尝试获得锁 tryLock()】
- tryLock() 表示当前线程尝试去获得锁,若获得锁成功,则返回 true;若获得锁失败,则返回 false。

- 这里用到的 nonfairTryAcquire 在上面 NonfairSync(非公平锁)实现方法中已经讲过了,但它属于 AQS 的部分,不属于 NonfairSync 的实现。可以看到 tryLock() 只会尝试一次去获得锁,并没有加入 CHL 锁队列中。

【可以抛出异常的加锁方法 lockInterruptibly()】
- lockInterruptibly() 方法的实现逻辑与 lock() 大致相同,lockInterruptibly() 遇到线程中断的情况会抛出 InterruptedException 异常,lock() 不会抛出任何异常。


- 同样加入 CHL 队列锁,不同的是 线程中断的时候,以下方法会抛出 InterruptedException 异常。

【解锁操作 unlock()】
- unlock() 方法用于释放锁。如果当前线程是锁的持有者,则将持有锁的数量减 1;若持有锁的数量为 0,则锁已经被释放;如果当前线程不是锁的持有者,调用 unlock() 方法会抛出 IllegalMonitorStateException 异常。

- 使用 tryRelease 释放锁,如果释放锁成功,则返回 true;释放锁失败则返回 false。

- tryRelease 方法中首先判断当前线程是否持有锁(即判断当前线程是否为独占锁的线程),如果不是则抛出 IllegalMonitorStateException 异常。getState() 返回当前线程持有锁的数量,减去 releases (1) 得到释放锁后当前线程持有锁的数量 c。
- 如果 c 为 0,则说明当前线程已经释放了锁,设置 free 为 true,设置独占锁的线程为 null。
- 更新锁的状态为 c,返回是否释放锁 free。

- tryRelease 返回 true,若 head 节点不为空且其 waitStatus(等待状态)不为 0,则调用 unparkSuccessor 唤醒队列的第一个线程节点

B9 Concurrent 重入锁(ReentrantLock)的更多相关文章
- synchronized关键字,Lock接口以及可重入锁ReentrantLock
多线程环境下,必须考虑线程同步的问题,这是因为多个线程同时访问变量或者资源时会有线程争用,比如A线程读取了一个变量,B线程也读取了这个变量,然后他们同时对这个变量做了修改,写回到内存中,由于是同时做修 ...
- 轻松学习java可重入锁(ReentrantLock)的实现原理
转载自https://blog.csdn.net/yanyan19880509/article/details/52345422,(做了一些补充) 前言 相信学过java的人都知道 synchroni ...
- 轻松学习java可重入锁(ReentrantLock)的实现原理(转 图解)
前言 相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么 ...
- 17_重入锁ReentrantLock
[概述] 重入锁可以完全代替synchronized关键字. 与synchronized相比,重入锁ReentrantLock有着显示的操作过程,即开发人员必须手动指定何时加锁,何时释放锁,所以重入锁 ...
- Java 重入锁 ReentrantLock 原理分析
1.简介 可重入锁ReentrantLock自 JDK 1.5 被引入,功能上与synchronized关键字类似.所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生 ...
- java 可重入锁ReentrantLock的介绍
一个小例子帮助理解(我们常用的synchronized也是可重入锁) 话说从前有一个村子,在这个村子中有一口水井,家家户户都需要到这口井里打水喝.由于井水有限,大家只能依次打水.为了实现家家有水喝,户 ...
- Java 显示锁 之 重入锁 ReentrantLock(七)
ReentrantLock 重入锁简介 重入锁 ReentrantLock,顾名思义,就是支持同一个线程对资源的重复加锁.另外,该锁还支持获取锁时的公平与非公平性的选择. 重入锁 ReentrantL ...
- Java多线程——深入重入锁ReentrantLock
简述 ReentrantLock 是一个可重入的互斥(/独占)锁,又称为“独占锁”. ReentrantLock通过自定义队列同步器(AQS-AbstractQueuedSychronized,是实现 ...
- Java多线程系列——深入重入锁ReentrantLock
简述 ReentrantLock 是一个可重入的互斥(/独占)锁,又称为“独占锁”. ReentrantLock通过自定义队列同步器(AQS-AbstractQueuedSychronized,是实现 ...
随机推荐
- java.lang.ClassNotFoundException: org.springframework.kafka.core.KafkaAdmin
<dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring ...
- 从ServerSwitch到SONiC Chassis:数据中心交换机技术的十年探索历程
从ServerSwitch到SONiC Chassis:数据中心交换机技术的十年探索历程 2019-07-09 | 作者:白巍 编者按:微软交换机操作系统开源项目SONiC (Software f ...
- A^B Mod C (快速幂)
题目描述: 给出3个正整数A B C,求A^B Mod C. 例如,3 5 8,3^5 Mod 8 = 3. Input3个正整数A B C,中间用空格分隔.(1 <= A,B,C <= ...
- C++标准库分析总结(四)——<Vector、Array、Forward_list设计原则>
本节主要总结标准库Vector和Array的设计方法和特性以及相关迭代器内部特征 1.Vector 1.1 Vector 内部实现 Vector是自增长的数组,其实在标准库中没有任何一种容器能原地扩充 ...
- Jetty - Unable to compile class for JSP
问题与分析 在启动公司项目时发现报错如下: [jetty] 2019-10-07 10:28:28.760:WARN:org.apache.jasper.compiler.Compiler:Error ...
- 小程序自定义底部tab
首页wxml的代码: <view class="nav" hover-class="none"> <view class="inde ...
- Mixed Content: The page at ‘https://XXX’ was loaded over HTTPS, but requested an insecure........
iframe引入视频的文件的时候报这个错 其实只要改成 加上一个s就好了 ...
- FM与FFM深入解析
因子机的定义 机器学习中的建模问题可以归纳为从数据中学习一个函数,它将实值的特征向量映射到一个特定的集合中.例如,对于回归问题,集合 T 就是实数集 R,对于二分类问题,这个集合可以是{+1,-1}. ...
- BAT文件运行时不显示命令窗口的方法
可以编一个VBS文件调用BAT文件,使运行BAT文件时不显示命令窗口. 新建一个记事本文件,保存为abc.vbs,在文件中加入如下代码: Set shell = Wscript.createobjec ...
- Android Studio 3.5新特性
Android Studio 3.5新特性 原文链接:https://blog.csdn.net/jklwan/article/details/99974869 Android Studio ...