问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

我们一起学并发编程:Java内存模型(五)锁的内存语义

发布网友 发布时间:2024-09-28 06:38

我来回答

1个回答

热心网友 时间:2024-10-20 10:07

简介:

锁的作用是让临界区互斥执行。本文阐述所得另一个重要知识点——锁的内存语义。

1、锁的释放-获取建立的happens-before关系

锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

锁释放-获取的示例代码:

packagecom.lizba.p1;/***<p>*锁示例代码*</p>**@Author:Liziba*@Date:2021/6/1021:43*/publicclassMonitorExample{inta=0;publicsynchronizedvoidwriter(){//1;a++;//2;}//3;publicsynchronizedvoidreader(){//4;inti=a;//5;System.out.println(i);}//6;}

假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规范,这个过程包含的happens-before关系可以分为3类。

根据程序次序规则:1happens-before2,2happens-before3,4happens-before5,5happens-before6

根据监视器锁规则:3happens-before4

根据happens-before的传递性,2happens-before5

上述happens-before关系的图形化表现形式如图:

总结:

线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立即变得对B线程可见。

2、锁释放和获取的内存语义

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。以上述MonitorExample程序为例,A线程释放锁后,共享数据的状态示意图如下所示:

共享数据的状态示意图

当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器锁保护的临界区代码必须从主内存中读取共享变量。

锁获取的状态示意图

对比锁释放-获取锁的内存语义与volatile写-读的内存语义可以看出:锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。

总结:

线程A释放锁,实质上是线程A向接下来要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。

线程B获取锁,实质上是线程B接受了之前某个线程发出的(在释放这个锁对共享变量锁做的修改的)消息。

线程A是否锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

3、锁内存的语义实现

分析ReentrantLock的源代码,来分析锁内存语义的具体实现机制。

示例代码:

packagecom.lizba.p1;importjava.util.concurrent.locks.ReentrantLock;/***<p>*ReentrantLock示例代码*</p>**@Author:Liziba*@Date:2021/6/1022:17*/publicclassReentrantLockExample{inta=0;ReentrantLocklock=newReentrantLock();publicvoidwriter(){lock.lock();//获取锁try{a++;}finally{lock.unlock();//释放锁}}publicvoidreader(){lock.lock();//获取锁try{inti=a;System.out.println(i);}finally{lock.unlock();//释放锁}}}

在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronized(AQS)。AQS使用一个整型的volatile变量(state)来维护同步状态,这个volatile变量是ReentrantLock内存语义实现的关键。

ReetrantLock的类图

ReentrantLock分为公平锁和非公平锁,首先分析公平锁。

使用公平锁时,加锁方法lock()的调用轨迹如下。

ReentrantLock:lock()

FairSync:lock()

AbstractQueuedSynchronizer:acquire(intarg)

ReentrantLock:tryAcquire(intacquires)

第4步开始真的加锁,下面是该方法的源代码:

protectedfinalbooleantryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();//获取锁开始,首先读取volatile变量stateintc=getState();if(c==0){if(!hasQueuedPredecessors()&&compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);returntrue;}}elseif(current==getExclusiveOwnerThread()){intnextc=c+acquires;if(nextc<0)thrownewError("Maximumlockcountexceeded");setState(nextc);returntrue;}returnfalse;}

从上面的代码中可以看出,加锁方法首先读取volatile变量state。

在使用公平锁时,解锁方法unlock()调用轨迹如下:

ReentrantLock:unlock()

AbstractQueuedSynchronizer:release(intarg)

Sync:tryRelease(intrelease)

第3步开始真的释放锁,下面是该方法的源代码:

protectedfinalbooleantryRelease(intreleases){intc=getState()-releases;if(Thread.currentThread()!=getExclusiveOwnerThread())thrownewIllegalMonitorStateException();booleanfree=false;if(c==0){free=true;setExclusiveOwnerThread(null);}//释放锁的最后,写volatile变量statesetState(c);returnfree;}

从上面的代码中可以看出,释放锁的最后写volatile变量state。

总结公平锁:

根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取到同一个volatile变量后将立即变得对获取锁的线程可见。

现在分析非公平锁:

注意,非公平锁的释放和公平锁的释放完全一致,都是上面的源代码。所以下面只分析非公平锁的获取过程。

使用非公平锁,加锁方法lock()的调用轨迹如下:

ReentrantLock:lock()

NonfairSync:lock()

AbstractQueuedSynchronizer:compareAndSetState(intexpect,intupdate)

第3步开始真的加锁,下面是该方法的源代码:

//方法1finalbooleannonfairTryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();intc=getState();if(c==0){//此方法中开始加锁if(compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);returntrue;}}elseif(current==getExclusiveOwnerThread()){intnextc=c+acquires;if(nextc<0)//overflowthrownewError("Maximumlockcountexceeded");setState(nextc);returntrue;}returnfalse;}//方法2protectedfinalbooleancompareAndSetState(intexpect,intupdate){//Seebelowforintrinsicssetuptosupportthis//该方法是native方法,在JVM中实现returnunsafe.compareAndSwapInt(this,stateOffset,expect,update);}

该方法以原子操作的方式更新state变量,也就是compareAndSet()(CAS)操作。JDK文档对该方法说明如下:如果当前状态值等于预期值,则以原子方式同步状态设置为给定更新的值。此操作具有volatile读和写的内存语义。

接下来分别从编译器和处理器的角度来分析,CAS如何同时具有volatile读和volatile写的内存语义。

编译器的角度:

前文已经讲过,编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写和volatile写后前面的任意内存操作重排序。组合这两个条件,意味着同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面任意内存操作重排序。

处理器的角度:

(本人不太懂C++)这一块总结需要看JVM源码,可能会总结错误,如需要深入理解这一块请查看《Java并发编程艺术》53页。

sun.misc.Unsafe中的compareAndSwapInt源码如下:(不懂Unsafe请看往期文章)

publicfinalnativebooleancompareAndSwapInt(Objectvar1,longvar2,intvar4,intvar5);

这是一个本地方法。这个本地方法会在openJDK中调用C++代码,假设当前是X86处理器,程序会根据当前处理器的类型来决定是非cmpxchg指令添加lock前缀。如果:

程序运行在多处理器上,就为cmpxchg指令加上lock前缀(LockCmpxchg)

程序运行在单处理器上,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)

intel手册对lock前缀的说明

对内存的读-改-写操作原子执行。(总线锁定/缓存锁定)

禁止该指令,与之前的读和写指令重排序

把写缓冲区的所有数据刷新到内存中

上面的2、3两点所具有的内存屏障的效果,足以同时实现volatile读和volatile写的内存语义。所以JDK文档说CAS具有volatile读和volatile写的内存语义对于处理器也是符合的。

公平锁和非公平锁的总结

公平锁和非公平锁的释放,最后都需要写一个volatile变量state

公平锁获取时,首先会去读volatile变量

非公平锁获取锁时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义

释放锁-获取锁的内存语义的实现方式总结

利用volatile变量的写-读所具有的内存语义

利用CAS所附带的volatile读和volatile写的内存语义

4、concurrent包的实现

由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信方式有以下4种方式

A线程写volatile变量,随后B线程读这个volatile变量

A线程写volatile变量,随后B线程用CAS更新这个volatile变量

A线程利用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量

A线程利用CAS更新一个volatile变量,随后B线程读这个volatile变量

Java的CAS会使用现代处理器上提供的高效机器级别的原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器实现同步的关键。同时volatile变量的读/写和CAS可以实现线程之间的通信。这些特性就是Java整个concurrent包的基石。

concurrent包的通用化实现模式

声明共享变量volatile

使用CAS的原子条件更新来实现线程之间的同步

配合volatile的读/写和CAS具有的volatile读和写的内存语义来实现线程之间的通信。

AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)、非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中基础类都是使用这个模式来实现的,而concurrent包中的高层类又是依赖于这些基础类。

图示concurrent包的实现示意图

concurrent包的实现示意图

文章总结至《Java并发编程艺术》,下篇总结“final域的内存语义”,敬请关注。

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
哪个牌子复印机好 复印店用什么型号的复印机好 开复印店需要什么设备 家用打印复印机哪个好 如何分辨鞋底是不是空心格子底? Ubuntu10.04下安装Oracle11g 超市监控多少钱 超市防盗器要多少钱 超市防盗系统多少钱 智能存放柜管理系统 48小时核酸检测结果在哪里查询-48小时核酸检测怎么看结果 什么树秋天不会落叶 有一部电视剧我只记得一点剧情了 好像是开头的剧情 讲的是一个神偷偷... 找一部老的青春偶像电视剧只记得女主角开始只是一个很平凡的人后来被... 江景房的优点是什么 QQ隐私空间怎么关闭,以前设置的,但现在忘了在哪里可以关闭了,每次... 三天在成都除了一天玩市内,还能去就近去那里。先谢过。 西岭雪山2日游的景区内酒店 ...请教各位大虾,从天津杨村开车,最近的路怎么走,到那住宿旅店相关事宜... 地下城堡2古堡秘境的岔路口-地下城堡2秘径的岔路口怎么过 曲阜道与南京路环岛修了,怎么走? 结石微创手术一般需几天出院 尿路结石微创需要住院几天 尿结石微创手术住几天院 输尿管结石手术需要住院几天 谁知道天敏数码相框是按什么排序的啊 相片 她不是按001 002 003的顺序... 天敏DPF71A电子相框 视频格式 天敏DPF700S其它参数 又纠结了,5D3用定焦,50还是85好 女人是不是喜欢男人才会主动找男人? 郑板桥劝舅父,说明郑板桥是个怎样的孩子? 深入synchronized底层原理 Java中,成员变量、静态变量和局部变量的线程安全性分析? 墙纸自带胶怎么帖 黄圣依在《婆婆和妈妈》里学习冲浪,她和婆婆的关系怎么样? 黄圣依人美情商高,平日里她都是怎么和婆婆相处的? 黄圣依婆家多有钱 哪几个方面可以看出黄圣依婆婆情商高? 玲珑轮胎750―16六层级能载重多少? 时代货车上用的轮胎650―16。路野牌、泸河牌、驭通牌、哪一种比较好... 650―16的轮胎打几个气压合适? 美团电单车工作人员是安保么? 演员黄圣依与哪位著名经济学家是亲戚关系 为什么从装的系统,找不到一些常用的办公软件 zcusn5pb5zn5中Z什么意思 机械里ZGuSn5Z5Pb5是什么意思 Cu85Sn5obzn5 So-50-sp-2 Cu85Sn5Pb5Zn5 这三种是什么材料 螺丝拧紧扭力国家标准是什么,套筒拧紧扭力国家标准最新 哪里有螺丝扭力国家标准对照表? 用手机拍的视频怎么传到电脑上看不到呢? 为什么手机的视屏文件到电脑放不出来啊?