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核心复习——线程池ThreadPoolExecutor源码分析
一.线程池的介绍 线程池一种性能优化的重要手段.优化点在于创建线程和销毁线程会带来资源和时间上的消耗,而且线程池可以对线程进行管理,则可以减少这种损耗. 使用线程池的好处如下: 降低资源的消耗 提高响 ...
- How To Install P4 Tutorials
安装一些依赖 sudo apt-get update sudo apt-get upgrade sudo apt-get install automake cmake libjudy-dev libp ...
- Linux中系统状态检测命令
1.ifconfig用于获取网卡配置与网络状态等信息,格式为:ifconfig [网络设备] [参数] 2.uname命令用于查看系统内核版本等信息,格式为:uname [-a] 查看系统的内核名称. ...
- 项目管理工具-OmniPlan 3 for Mac
链接:https://pan.baidu.com/s/1tp_37fHXHwJuklL1nNSwig 密码:l0sf 激活迷药(里面自带的keygen不能用,用这个好使): Name: Appked ...
- 流式数据处理在百度数据工厂的应用与实践 原创: 李俊卿 AI前线 今天
流式数据处理在百度数据工厂的应用与实践 原创: 李俊卿 AI前线 今天
- Python 中的 -> 是什么意思
函数标注通常用于 类型提示:例如以下函数预期接受两个 int 参数并预期返回一个 int 值:```def sum_two_numbers(a: int, b: int) -> int:retu ...
- java匿名内部类new(){}
匿名内部类:顾名思义,没有名字的内部类.表面上看起来它们似乎有名字,实际那不是它们的名字.当程序中使用匿名内部类时,在定义匿名内部类的地方往往直接创建该类的一个对象.匿名内部类的声明格式如下:new ...
- Mac中好用的快捷键
1.safari safariy页面刷新:Command+R,类似于Win系统里面的F5
- SVG-概述/容器与通用属性
参考: SVG 图像入门教程 MDN SVG SVG教程 SVG入门-踏得 工具: svg在线编辑 概述 SVG 是一种基于 XML 语法的图像格式,全称是可缩放矢量图(Scalable Vector ...
- [原][OSG][osgEarth]osgEarth例子程序简介
1.osgearth_graticule:生成经纬线. 2.osgearth_annotation:各类标注(点.线.面.模型.文本等). 3.osgearth_city:加载一个城市三维模型,可以浏 ...