聊聊Java 可重入锁的那些事
yuyutoo 2024-12-12 15:53 1 浏览 0 评论
本文主要包含的内容:可重入锁(ReedtrantLock)、公平锁、非公平锁、可重入性、同步队列、CAS等概念的理解
通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都需要先获取锁;而有一些锁可能允许并发访问共享资源。
本文主要讲解可重入锁(ReentrantLock),该锁为独占共享资源锁,即独占锁。
1.可重入锁(ReentrantLock)
可重入锁指的是同一个线程可无限次地进入同一把锁的不同代码,又因该锁通过线程独占共享资源的方式确保并发安全,又称为 独占锁 。
举个例子:同一个类中的synchronize关键字修饰了不同的方法。synchronize是内置的隐式的可重入锁,例子中的两个方法使用的是同一把锁,只要能执行testB()也就说明线程拿到了锁,所以执行testA()方法就不用被阻塞等待获取锁了;如果不是同一把锁或非可重入锁,就会在执行testA()时被阻塞等待。
<pre class="prettyprint hljs java" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">public class Demo {
public synchronized void testA(){
System.out.println("执行测试A");
}
public synchronized void testB(){
System.out.println("执行测试B");
testA();
}
}
1.1.可重入锁的类图关系
ReentrantLock实现了 Lock 接口和 Serializable 接口(都没画出来),它有三个内部类( Sync 、 NonfairSync 、 FairSync ), Sync 是一个抽象类,它继承 AbstractQueuedSynchronizer 抽象同步队列 ,同时有两个实现类( NonfairSync 和 FairSync ),其中父类 AQS 是个模板类提供了许多以锁相关的操作,子类分别是两种不同的获取锁实现( 非公平锁和公平锁 )。AQS 又继承了 AbstractOwnableSynchronizer 类, AOS 用于保存锁被 独占 的线程对象。
ReentrantLock 类的构造方法有如下两种,很显然,在对象实例化时将决定同步器Sync是公平还是非公平。
<pre class="prettyprint hljs java" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// ReentrantLock类
private final Sync sync;
// 默认非公平
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
先关注ReentrantLock类的方法lock() 和 unlock()。从源码可以发现 ReentrantLock类的方法是交给内部类Sync 类来实现 ,而lock()方法在Sync类中是个抽象方法,具体实现在子类FairSync和NonfairSync类。其实ReentrantLock类中的其他方法也是交给Sync类去处理的,所以想要理解ReentrantLock类的重点是理解Sync类。
注意一个点:Sync类中lock()抽象方法不是Lock接口的抽象方法,它们是通过调用(如下:point_down:)代码产生关联的。
<pre class="prettyprint hljs cs" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// java.util.concurrent.locks.ReentrantLock类
public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
结论一:
- ReentrantLock 可重入锁 获取锁 有两种实现:公平和非公平;注意:从类图关系我们可以知道,公平和非公平内部类只有两个方法,都是与获取锁有关,公平与否仅针对获取锁而言,也即是lock()方法。PS:tryAcquire(int)最终会被lock()调用。
- ReentrantLock的理解重点源码应该关注内部同步器Sync类和Sync的父类抽象同步队列AbstractQueuedSynchronizer。
1.2.怎么使用ReentrantLock
使用案例:并发安全访问共享资源
<pre class="prettyprint hljs java" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">public class LockDemo {
public static void main(String[] args) {
// 简单模拟20人抢优惠
for(int i=0;i<20;i++){
new Thread(new ThreadDemo()).start();
}
}
}
// 前十位可以获取优惠,凭号码兑换优惠
class ThreadDemo implements Runnable{
private static Integer num = 10;
private static final ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取锁
reentrantLock.lock();
try {
if(num<=0){
System.out.println("已被抢完,下次再来");
return;
}
System.out.println(Thread.currentThread().getName()+"用户抢到的号码:"+num--);
}finally {
// 释放锁
reentrantLock.unlock();
}
}
}
执行结果:
Thread-18用户抢到的号码:10
Thread-14用户抢到的号码:9
Thread-15用户抢到的号码:8
Thread-4用户抢到的号码:7
Thread-1用户抢到的号码:6
Thread-19用户抢到的号码:5
Thread-11用户抢到的号码:4
Thread-17用户抢到的号码:3
Thread-16用户抢到的号码:2
Thread-13用户抢到的号码:1
已被抢完,下次再来
已被抢完,下次再来
……
常用的一些方法
2.一些概念的理解
2.1.锁和同步队列的关系
前面讲述过:ReentrantLock类的方法都是交给内部类Sync类来实现的。
Sync和它的子类都实现了,为什么还要ReentrantLock类来套这么一层呢?这关系到锁的使用和实现的问题。
- 锁是面向开发者,隐藏细节让锁的开发变得更简洁;
- 抽象同步队列是面向锁的实现,屏蔽了同步状态的管理、线程的排队、等待与唤醒等底层操作,简化了自定义同步器和锁的实现。
说白了,ReentrantLock(锁)类为了简化开发者的使用,具体实现交由其内部类自定义的同步器Sync去处理,而AQS则以模板的方式提供一系列有关锁的操作及部分可被子类Sync重写的模板方法。
2.2.公平锁与非公平锁概述
公平与非公平指的是获取锁的机制不同。
公平锁强调先来后到,表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定,即同步队列记录线程先后顺序,队列的特性FIFO(先进先出);
非公平锁只要CAS设置同步状态成功,当前线程就会获取到锁,没获取成功的依然放在同步队列中按FIFO原则等待,等待下一次的CAS操作。
从源码上可以知道它们的主要区别是多一个判断: !hasQueuedPredecessors()
该判断表示:加入了同步队列中当前节点是否有前驱节点,即在同步队列中有没有比当前线程更早的线程在队列中等待了,而 非公平锁是没有这个判断的 。
<pre class="prettyprint hljs java" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">// java.util.concurrent.locks.ReentrantLock.NonfairSync
// 非公平
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// java.util.concurrent.locks.ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
// java.util.concurrent.locks.ReentrantLock.FairSync
// 公平:比非公平多了一步判断 !hasQueuedPredecessors()
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 主要区别:!hasQueuedPredecessors()
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;
}
附上 获取锁时 公平锁和非公平锁的源码区别图
结论二:
公平锁和非公平锁的主要区别是: !hasQueuedPredecessors() ,表示同步队列中当前节点是否有前驱节点,即在同步队列中有没有比当前线程更早的线程在队列中等待了,而 非公平锁没有这个判断 。
2.3.实现锁的可重入特性
前面在 公平锁与非公平锁概述 这点中,附上了对比两者的关键源码,其中可重入的源码是一样的:point_down:
<pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">......
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
判断当前线程和当前拥有独占访问权限的线程对比,是同一个线程则可以重新进入同一把锁。处理逻辑是:对同步状态state加上acquires=1,然后返回true,返回true即获取锁成功。
AbstractOwnableSynchronizer类用于保存 锁被独占的线程对象 ,AOS类只有以下两个方法:
- Thread getExclusiveOwnerThread()为获取当前拥有独占访问权限的线程,
- void setExclusiveOwnerThread(Thread)为设置当前拥有独占访问权限的线程。
所以每次在获取锁成功后会做这么一步: setExclusiveOwnerThread(current) :point_down:
<pre class="prettyprint hljs bash" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
ReentrantLock的内部类Sync继承AQS实现模板方法 tryRelease(int) 实现锁的释放规则,源码如下:point_down:方法参数releases=1。
先判断该线程是否为当前拥有独占访问权限的线程,再判断同步状态,如果状态不为0,则锁还没释放完,不执行 setExclusiveOwnerThread(null) 即不释放独占访问权限的线程。因为发生锁的重入时,同步状态state>1,所以锁释放时同步状态需要一层层出来,直到同步状态为0时,才会置空拥有独占访问权的线程。因此AQS的state状态表示锁的持有次数。
<pre class="prettyprint hljs java" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
结论三:公平和非公平的可重入性都一样,并且同步状态state的作用如下
- 同步状态state<0 表示 throw new Error("Maximum lock count exceeded");
- 同步状态state=0 表示锁没有被占用
- 同步状态state=1 表示锁被占用了
- 同步状态state>1 表示锁发生了重新进入
即同步状态state等于锁持有的次数。
2.4.CAS概述
CAS的全称是Compare And Swap,意思是 比较并交换 ,是一种特殊的处理器指令。
以方法compareAndSetState(int expect,int update)为例:
处理逻辑是:期望参数expect值跟内存中当前状态值 比较 ,等于则 原子性的修改 state值为update参数值。
获取锁操作:compareAndSetState(0, 1),当同步状态state=0时,则修改同步状态state=1
compareAndSetState() 方法调用了Unsafe 类下的本地方法compareAndSwapInt(),该方法由JVM实现CAS一组汇编指令,指令的执行必须是连续的不可被中断的,不会造成所谓的数据不一致问题,但只能保证一个共享变量的 原子性操作 。
同步队列中还有很多CAS相关方法,比如:
compareAndSetWaitStatus(Node,int,int):等待状态的原子性修改
compareAndSetHead(Node):设置头节点的原子性操作
compareAndSetTail(Node, Node):从尾部插入新节点的原子性操作
compareAndSetNext(Node,Node,Node):设置下一个节点的原子性操作
除了同步队列中提供的CAS方法,在Java并发开发包中,还提供了一系列的CAS操作,我们可以使用其中的功能让并发编程变得更高效和更简洁。
java.util.concurrent.atomic 一个小型工具包,支持 单个变量 上的无锁线程安全编程。
比如:num++ 或num--,自增和自减这些操作是非原子性操作的,无法确保线程安全,为了提高性能不考虑使用锁(synchronized、Lock),可以使用AtomicInteger类的方法来完成自增、自减,其本质是CAS原子性操作。
<pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">AtomicInteger num = new AtomicInteger(10);
// 自增
System.out.println(num.getAndIncrement());
// 自减
System.out.println(num.getAndDecrement());
注意:只是在自增和自减的过程是原子性操作。
如下代码:point_down:下面整块代码是非线程安全的,只是 num.getAndDecrement() 自减时是原子性操作,也即是并发场景下num.get()无法确保获取到最新值。
<pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">private static AtomicInteger num = new AtomicInteger(10);
......
if(num.get()<=0){
System.out.println("已被抢完,下次再来");
return;
}
System.out.println("号码:"+num.getAndDecrement());
支持哪些数据类型呢?
基本数据类型
- AtomicBoolean:原子更新布尔值类型
- AtomicInteger:原子更新整数类型
- AtomicLong:原子更新长整型
数组类型
- AtomicIntegerArray:原子更新整型数组里的元素
- AtomicLongArray:原子更新长整型数组里的元素
- AtomicReferenceArray:原子更新引用类型数组里的元素
引用类型
- AtomicReference:原子更新引用类型
- AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
更新类型中的字段
- AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段
3.抽象同步队列AQS
AbstractQueuedSynchronizer 抽象同步队列,它是个模板类提供了许多以锁相关的操作,常说的AQS指的就是它。AQS继承了 AbstractOwnableSynchronizer 类,AOS用于保存线程对象,保存什么线程对象呢? 保存锁被独占的线程对象 。
抽象同步队列AQS除了实现序列化标记接口,并没有实现任何的同步接口,该类提供了许多同步状态获取和释放的方法给自定义同步器使用,如ReentrantLock的内部类Sync。抽象同步队列支持独占式或共享式的的获取同步状态,方便实现不同类型的自定义同步器。一般方法名带有 Shared 的为共享式,比如,尝试以共享式的获取锁的方法 int tryAcquireShared(int) ,而独占式获取锁方法为 boolean tryAcquire(int) 。
AQS是抽象同步队列,其重点就是 同步队列 及 如何操作同步队列 。
3.1同步队列
双向同步队列,采用尾插法新增节点,从头部的下一个节点获取操作节点,节点自旋获取同步锁,实现FIFO(先进先出)原则。
理解节点中的属性值作用
- prev:前驱节点;即当前节点的前一个节点,之所以叫前驱节点,是因为前一个节点在使用完锁之后会解除后一个节点的阻塞状态;
- next:后继节点;即当前节点的后一个节点,之所以叫后继节点,是因为“后继有人”了,表示有“下一代”节点承接这个独有的锁:lock:;
- nextWaiter:表示指向下一个 Node.CONDITION 状态的节点(本文不讲述Condition队列,在此可以忽略它);
- thread:节点对象中保存的线程对象,节点都是配角,线程才是主角;
- waitStatus:当前节点在队列中的等待状态
因篇幅原因,关于抽象同步队列AQS、锁的获取过程、锁的释放过程、自旋锁、线程阻塞与释放、线程中断与阻塞关系等内容将在下一篇文章展开讲解。
:point_down:图是新增节点的过程
相关推荐
- 史上最全的浏览器兼容性问题和解决方案
-
微信ID:WEB_wysj(点击关注)◎◎◎◎◎◎◎◎◎一┳═┻︻▄(页底留言开放,欢迎来吐槽)●●●...
-
- 平面设计基础知识_平面设计基础知识实验收获与总结
-
CSS构造颜色,背景与图像1.使用span更好的控制文本中局部区域的文本:文本;2.使用display属性提供区块转变:display:inline(是内联的...
-
2025-02-21 16:01 yuyutoo
- 写作排版简单三步就行-工具篇_作文排版模板
-
和我们工作中日常word排版内部交流不同,这篇教程介绍的写作排版主要是用于“微信公众号、头条号”网络展示。写作展现的是我的思考,排版是让写作在网格上更好地展现。在写作上花费时间是有累积复利优势的,在排...
- 写一个2048的游戏_2048小游戏功能实现
-
1.创建HTML文件1.打开一个文本编辑器,例如Notepad++、SublimeText、VisualStudioCode等。2.将以下HTML代码复制并粘贴到文本编辑器中:html...
- 今天你穿“短袖”了吗?青岛最高23℃!接下来几天气温更刺激……
-
最近的天气暖和得让很多小伙伴们喊“热”!!! 昨天的气温到底升得有多高呢?你家有没有榜上有名?...
- CSS不规则卡片,纯CSS制作优惠券样式,CSS实现锯齿样式
-
之前也有写过CSS优惠券样式《CSS3径向渐变实现优惠券波浪造型》,这次再来温习一遍,并且将更为详细的讲解,从布局到具体样式说明,最后定义CSS变量,自定义主题颜色。布局...
- 你的自我界限够强大吗?_你的自我界限够强大吗英文
-
我的结果:A、该设立新的界限...
- 行内元素与块级元素,以及区别_行内元素和块级元素有什么区别?
-
行内元素与块级元素首先,CSS规范规定,每个元素都有display属性,确定该元素的类型,每个元素都有默认的display值,分别为块级(block)、行内(inline)。块级元素:(以下列举比较常...
-
- 让“成都速度”跑得潇潇洒洒,地上地下共享轨交繁华
-
去年的两会期间,习近平总书记在参加人大会议四川代表团审议时,对治蜀兴川提出了明确要求,指明了前行方向,并带来了“祝四川人民的生活越来越安逸”的美好祝福。又是一年...
-
2025-02-21 16:00 yuyutoo
- 今年国家综合性消防救援队伍计划招录消防员15000名
-
记者24日从应急管理部获悉,国家综合性消防救援队伍2023年消防员招录工作已正式启动。今年共计划招录消防员15000名,其中高校应届毕业生5000名、退役士兵5000名、社会青年5000名。本次招录的...
- 一起盘点最新 Chrome v133 的5大主流特性 ?
-
1.CSS的高级attr()方法CSSattr()函数是CSSLevel5中用于检索DOM元素的属性值并将其用于CSS属性值,类似于var()函数替换自定义属性值的方式。...
- 竞走团体世锦赛5月太仓举行 世界冠军杨家玉担任形象大使
-
style="text-align:center;"data-mce-style="text-align:...
- 学物理能做什么?_学物理能做什么 卢昌海
-
作者:曹则贤中国科学院物理研究所原标题:《物理学:ASourceofPowerforMan》在2006年中央电视台《对话》栏目的某期节目中,主持人问过我一个的问题:“学物理的人,如果日后不...
-
- 你不知道的关于这只眯眼兔的6个小秘密
-
在你们忙着给熊本君做表情包的时候,要知道,最先在网络上引起轰动的可是这只脸上只有两条缝的兔子——兔斯基。今年,它更是迎来了自己的10岁生日。①关于德艺双馨“老艺...
-
2025-02-21 16:00 yuyutoo
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)