Java中锁的实现和内存语义浅析
作者:蜗牛大师 发布时间:2021-06-04 01:09:49
1. 概述
锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程获取同一个锁的线程发送消息。
锁在实际使用时只是明白锁限制了并发访问, 但是锁是如何实现并发访问的, 同学们可能不太清楚, 下面这篇文章就来揭开锁的神秘面纱.
2. 锁的内存语义
当线程获取锁时, JMM会把线程对应的本地内存置为无效. 从而使得被监视器保护的临界区的变量必须从主内存中读取.
当线程释放锁时, JMM会把该线程对应的本地内存中的共享变量刷新到主内存中(并不是不释放锁就不刷新到主内存, 只是释放锁时把未刷新到主内存中的数据刷新到主内存).
锁的内存语义与volatile的内存语义
锁获取与volatile读有相同的内存语义.
锁释放与volatile写有相同的内存语义.
内存语义总结
线程A释放一个锁, 实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息.
线程B获取一个锁, 实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息.
线程A释放锁, 随后线程B获取这个锁, 这个过程实质上是线程A通过主内存向线程B发送消息.
3. 锁内存语义的实现
下面以ReentrantLock为例, 获取到锁就是把state改为1(不考虑重入), 释放锁时改为0.
而加锁的关键代码就是
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
该方法以原子操作的方式更新state变量, 本文把Java的compareAndSet()方法简称为CAS. JDK文档对该方法的说明如下: 如果当前状态值等于预期值, 则以原子方式将同步状态设置为给定的更新值. 此操作具有volatile读和写的内存语义.
这里我们分别从编译器和处理器的角度来分析: CAS如何同时具有volatile读和volatile写的内存语义.
我们知道, 编译器不会对volatile读与volatile读后面的任意内存操作重排序; 编译器不会对volatile写与volatile写前面的任意内存操作重排序. 组合这两个条件, 意味着为了同时实现volatile读和volatile写的内存语义, 编译器不能对CAS与CAS前面和后面的任意内存操作重排序.
下面我们来分析在常见的intel X86处理器中, CAS是如何同时具有volatile读和volatile写的内存语义的.
下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码.
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
可以看到, 这是一个本地方法调用. 这个本地方法在openjdk中依次调用的c++代码为: unsafe.cpp, atomic.cpp 和 atomic_windows_x86.inline.hpp. 这个本地方法的最终实现在openjdk的如下位置: openjdk-7-fcs-src-b147-
27_jun_2011\openjdk\hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp(对应于
Windows操作系统, X86处理器). 下面是对应于intel X86处理器的源代码的片段.
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
如上面源代码所示, 程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀. 如果程序是在多处理器上运行, 就为cmpxchg指令加上lock前缀(Lock Cmpxchg). 反之, 如果程序是在单处理器上运行, 就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性, 不需要lock前缀提供的内存屏障效果).
intel的手册对lock前缀的说明如下.
确保对内存的读-改-写操作原子执行. 在Pentium及Pentium之前的处理器中, 带有lock前缀的指令在执行期间会锁住总线, 使得其他处理器暂时无法通过总线访问内存. 很显然, 这会带来昂贵的开销. 从Pentium 4、Intel Xeon及P6处理器开始, Intel使用缓存锁定(Cache Locking)
来保证指令执行的原子性. 缓存锁定将大大降低lock前缀指令的执行开销.禁止该指令, 与之前和之后的读和写指令重排序.
把写缓冲区中的所有数据刷新到内存中.
上面的第2点和第3点所具有的内存屏障效果, 足以同时实现volatile读和volatile写的内存语义.
经过上面的分析, 现在我们终于能明白为什么JDK文档说CAS同时具有volatile读和volatile写的内存语义了.
从本文对ReentrantLock的分析可以看出, 锁释放-获取的内存语义的实现至少有下面两种方式.
利用volatile变量的写-读所具有的内存语义.
利用CAS所附带的volatile读和volatile写的内存语义.
4. 总结
对于锁, 可以这么理解, N个线程去通过CAS去修改一个volatile变量, 但是由于CPU提供的机制, 只能有一个线程修改成功, 修改成功的线程获得锁, 其它线程以及后来的线程要么自旋一会儿, 要么直接挂起, 等待获取锁的线程释放锁时去唤醒. 就是这么个过程.
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。
来源:https://www.cnblogs.com/wuqinglong/p/9962142.html


猜你喜欢
- 摘要:在spring boot中 MVC这部分也有默认自动配置,也就是说我们不用做任何配置,那么也是OK的,这个配置类就是 WebMvcAu
- 页面元素定位是自动化中最重要的事情, selenium Webdriver 提供了很多种元素定位的方法。 测试人员应该熟练掌握各
- 本文实例为大家分享了Android实现层叠卡片式banner的具体代码,供大家参考,具体内容如下效果图如下:背景由于公司VIP模块项目需要,
- Java执行cmd命令//当前绝对路径System.out.println(IoUtil.read(Runtime.getRuntime()
- 本文实例为大家分享了Unity3d简易五子棋源码,供大家参考,具体内容如下Unity3d部分对C#源码进行了改写简化:using Unity
- 实践过程pdf转excelpublic static long pdfToExcel(String inFile, String outFi
- 引言大家应该都知道,对Excel表格设置分页对我们预览、打印文档时是很方便的,特别是一些包含很多复杂数据的、不规则的表格,为保证打印时每一页
- 最常用的序列化是把某个类序列化成二进制文件.但有时我们也会把类序列化成xml文件. 假如有如下一个类 class Arwen { priva
- 说到事件机制,可能脑海中最先浮现的就是日常使用的各种 listener,listener去监听事件源,如果被监听的事件有变化就会通知list
- MybatisPlus特性•无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑•损耗小:启动即会自动注入基本 CURD,性能
- 实现功能实现使用FTP上传、下载、重命名、刷新、删除功能开发环境开发工具: Visual Studio 2013.NET Framework
- 代码很简单,这里就不多废话了,直接奉上代码using UnityEngine;using System.Collections;public
- RabbitMQ主要有六种种工作模式,本文整合SpringBoot分别介绍工作模式的实现。前提概念生产者消息生产者或者发送者,使用P表示:队
- 在C#中DateTime是一个包含日期、时间的类型,此类型通过ToString()转换为字符串时,可根据传入给Tostring()的参数转换
- Activities提供了一种方便管理的创建、保存、回复的对话框机制,例如 onCreateDialog(int), onPrepareDi
- 目录第一种方式第二种方式第三种方式第四种方式(缺点:将所有的数字类型都会转为字符串)web项目中,Java后端传过来的Long/long类型
- 相信大家对SaaS架构都有所了解,这里也不过多介绍,让我们直奔主题。技术框架springboot版本为2.3.4.RELEASE持久层采用J
- java 算法之快速排序实现代码摘要: 常用算法之一的快速排序算法的java实现原理:选择一个基准元素,通常选择第一个元素或者最后一个元素,
- 本文实例为大家分享了Android仿微信录制语音的具体代码,供大家参考,具体内容如下前言我把录音分成了两部分1.UI界面,弹窗读秒 2.一个
- 前言Android studio依赖项目是使用gradle管理的,依赖一个项目、一个jar包、一个工程,都可以在这里进行配置,本文将给大家详