Java 三大并大特性-可见性介绍(结合代码、分析源码)

目录

​编辑

一、可见性概念

1.1 概念

二、可见性问题由来

2.1 由来分析

三、可见性代码例子

3.1 代码

3.2 执行结果

四、Java 中保证可见性的手段

4.1 volatile

4.1.1 优化代码

4.1.2 测试结果

4.1.3 volatile原理分析

4.1.3.1 查看字节码

4.1.3.2 hotspot 层面

4.1.3.3 volatile原理总结

4.2 synchronized

4.2.1 代码优化

4.2.2 测试结果

4.2.3 synchronized 原理分析

4.2.3.1 synchronized 修饰方法

4.2.3.1.1 源代码

4.2.3.1.2 执行结果

4.2.3.1.3 编译分析

4.2.3.2 synchronized 修饰代码块

4.2.3.2.1 源代码

4.2.3.2.2 执行结果

4.2.3.2.3 编译分析

4.3 Lock

4.3.1 优化代码

4.3.2 测试结果

4.3.3 Lock实现可见性原理分析

4.3.3.1 源码分析

4.3.3.2 总结

4.4 final

4.4.1 final 实现可见性原理分析


一、可见性概念

1.1 概念

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。

二、可见性问题由来

2.1 由来分析

可见性问题是在CPU位置出现的,CPU处理速度非常快,相对CPU来说,去主内存获取数据这个事情太慢了,为了解决CPU加载主内存数据慢的问题,就在CPU加入了缓存寄存器,分别为L1、L2、L3三级缓存,每次去主内存拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。如下图所示:

引入这种缓存机制后,这就带来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致主内存和缓存之间数据不一致问题而数据不一致的问题,换据说法就是可见性问题。

为了解决CPU硬件层面的缓存一致性问题,于是就设计出了缓存一致性协议,其中比较典型的就是MESI协议,但是这个协议其实不同的CPU厂商的实现方式是有差异的,Java 层面为了屏蔽各种硬件和操作系统带来的差异,让并发编程做到真正意义上的跨平台,就设计出了JMM,即Java Memery Model, Java 内存模型来解决

三、可见性代码例子

3.1 代码

package com.ningzhaosheng.thread.concurrency.features.visible;/*** @author ningzhaosheng* @date 2024/2/5 19:36:39* @description 测试可见性*/
public class TestVisible {private static boolean flag = true;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag) {// ....}System.out.println("t1线程结束");});t1.start();Thread.sleep(10);flag = false;System.out.println("主线程将flag改为false");}
}

3.2 执行结果

由结果可知,主线程修改了flag = false;但是并没有使t1线程里面的循环结束。

四、Java 中保证可见性的手段

4.1 volatile

4.1.1 优化代码

package com.ningzhaosheng.thread.concurrency.features.visible.volatiles;/*** @author ningzhaosheng* @date 2024/2/5 19:45:29* @description 测试volatile*/
public class TestVolatile {private volatile static boolean flag = true;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag) {// ....}System.out.println("t1线程结束");});t1.start();Thread.sleep(10);flag = false;System.out.println("主线程将flag改为false");}
}

4.1.2 测试结果

由以上测试结果可以看到,使用volatile 修饰共享变量之后,在主线程修改了flag =false 之后,线程t1读取到了最新值,并结束了循环,结束了线程。那么为什么使用了volatile之后就能解决共享变量的问题呢?要回答这个问题其实综合性考虑的内容还比较多,涉及到CPU的多级缓存、计算机缓存一致性协议和Java 内存模型等相关内容。我们接下来就分析下吧。

4.1.3 volatile原理分析

4.1.3.1 查看字节码
javap -v .\TestVolatile.class

从以上截图我们可以看到,使用volatile修饰的变量,会多一个ACC_VOLATILE 指令关键字。我们接着去hotspot 查看c++源码,分析ACC_VOLATILE做了些什么操作。

4.1.3.2 hotspot 层面

根据ACC_VOLATILE指令关键字,我们可以在hotspot 源码中,找到他的内容:

jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/utilities/accessFlags.hpp (openjdk.org)

接着,我们找下is_volatile:

jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/interpreter/bytecodeInterpreter.cpp (openjdk.org)​​​​​​

 

从以上截图的这段代码中可以看到,会先判断tos_type(volatile变量类型),后面有不同的基础类型的调用,比如int类型就调用release_int_field_put,byte就调用release_byte_field_put等等。
判断完类型之后,我们可以看到代码后面执行的语句是:

我们可以在以下代码位置找到该源码:

jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/runtime/orderAccess.hpp (openjdk.org)

实际上storeload() 这个方法,针对不同CPU有不同的实现,它的具体实现在src/os_cpu下,我们可以去看一下:

这里我们以linux_x86架构的CPU实现为例,我们去看下storeload()方法做了些什么操作。

jdk8u/jdk8u/hotspot: 69087d08d473 src/os_cpu/linux_x86/vm/orderAccess_linux_x86.inline.hpp (openjdk.org)

接着看下fence()函数:

通过这面代码可以看到lock;add1,其实这个就是内存屏障。lock;add1 $0,0(%%esp)作为cpu的一个内存屏障。
add1 $0,0(%%rsp)表示:将数值0加到rsp寄存器中,而该寄存器指向栈顶的内存单元。加上一个0,rsp寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用add1指令来配合lock指令,用作cpu的内存屏障。

内存屏障:

这四个分别对应了经常在书中看到的JSR规范中的读写屏障LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  • LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

对于volatile操作而言,其操作步骤如下:

  • 每个volatile写入之前,插入一个 StoreStore ,写入以后插入一个StoreLoad,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中。
  • 每个volatile读取之前,插入一个 LoadLoad ,读取之后插入一个LoadStoreJMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量
4.1.3.3 volatile原理总结

通过编译的字节码分析,我们可以知道,使用volatile 修饰的变量,编译后会生成ACC_VOLATILE关键字,通过关键字搜索,我们在hotspot 源码层(JVM层)查询到,is_volatile函数,这个函数的作用就是会先判断tos_type(volatile变量类型),后面有不同的基础类型的调用,比如int类型就调用release_int_field_put,byte就调用release_byte_field_put等等,还有就是调用了一个OrderAccess::storeload();函数,最终我们通过查看源码,找到storeload方法在不同CPU架构下的实现,最终基本可以得出以下结论:

  • 在JVM层:volitile的底层,在JVM层其实是通过内存屏障防止了指令重排序。
  • CPU层面:在x86的架构中,含有lock前缀的指令拥有两种方法实现;一种是开销很大的总线锁,它会把对应的总线直接全部锁住,如此明显是不合理的;所以后期intel引入了缓存锁以及mesi协议,如此便可以轻量化的实现内存屏障;

最终结论:volatile的底层原理,在JVM源码层次而言,内存屏障直接起到了禁止指令重排的作用,且之后与总线锁或者MESI协议配合实现了可见性;即:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中;当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量。

4.2 synchronized

4.2.1 代码优化

package com.ningzhaosheng.thread.concurrency.features.visible.syn;/*** @author ningzhaosheng* @date 2024/2/5 19:52:31* @description 测试synchronized*/
public class TestSynchronized {private static boolean flag = true;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag) {synchronized (TestSynchronized.class) {//...}System.out.println(111);}System.out.println("t1线程结束");});t1.start();Thread.sleep(10);flag = false;System.out.println("主线程将flag改为false");}
}

4.2.2 测试结果

从测试结果可以看出,使用了synchronized同步代码块之后,在主线程中修改了flag=false 之后,线程t1也获取到最新的变量值,结束了while循环。也就是说synchronized也可以解决并发编程的可见性问题。那么synchronized是怎么保证并发编程的可见性的呢,我们接下来分析下。

4.2.3 synchronized 原理分析

4.2.3.1 synchronized 修饰方法
4.2.3.1.1 源代码
package com.ningzhaosheng.thread.concurrency.features.visible.syn;/*** @author ningzhaosheng* @date 2024/2/13 10:16:36* @description synchronized 修饰方法*/
public class TestSynchronizedMethod {public static boolean flag = true;public static synchronized void runwhile() {while (flag) {System.out.println(111);}System.out.println("t1线程结束");}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {runwhile();});t1.start();Thread.sleep(10);flag = false;System.out.println("主线程将flag改为false");}
}
4.2.3.1.2 执行结果

4.2.3.1.3 编译分析
javap -v .\TestSynchronizedMethod.class

可以看见,使用synchronized修饰方法后,通过javap -v 查看编译的字节码,会生成一个ACC_SYNCHRONIZED标识符,会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。

可查看官网解析:Chapter 2. The Structure of the Java Virtual Machine (oracle.com)

该标识符的作用是使当前线程优先获取Monitor对象,同一个时刻只能有一个线程获取到,在当前线程释放Monitor对象之前,其它线程无法获取到同一个Monitor对象,从而保证了同一时刻只能有一个线程进入到被synchornized修饰的方法

获取到锁资源之后,会将内部涉及到的变量从CPU缓存中移除,且要求线程必须去主内存中重新拿数据,在释放锁之后,会立即将CPU缓存中的数据同步到主内存。

注意:关于Monitor的更多底层实现原理,由于篇幅原因,这里先不分析,后续会出相关文章详细说明synchronized,这里只是就实现可见性原理做些说明。

4.2.3.2 synchronized 修饰代码块
4.2.3.2.1 源代码
package com.ningzhaosheng.thread.concurrency.features.visible.syn;/*** @author ningzhaosheng* @date 2024/2/13 10:48:02* @description synchronized 修饰代码块*/
public class TestSynchronizedCodeBlock {public static boolean flag = true;public static void runwhile() {while (flag) {synchronized (TestSynchronizedCodeBlock.class) {System.out.println(flag);}System.out.println(111);}System.out.println("t1线程结束");}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {runwhile();});t1.start();Thread.sleep(10);flag = false;System.out.println("主线程将flag改为false");}
}
4.2.3.2.2 执行结果

4.2.3.2.3 编译分析
javap -v .\TestSynchronizedCodeBlock.class

可以看到,使用synchronized修饰代码块后,查看编译的字节码会发现再存取操作静态共享变量时,会插入monitorenter、monitorexit原语指令,关于这两个指令的说明,可查看文档:

Chapter 6. The Java Virtual Machine Instruction Set (oracle.com)

它实现可见性的原理和上一小节说明的那样,都是:

当前线程优先获取Monitor对象,同一个时刻只能有一个线程获取到,在当前线程释放Monitor对象之前,其它线程无法获取到同一个Monitor对象,从而保证了同一时刻只能有一个线程进入到被synchornized修饰的代码块。

获取到锁资源之后,会将内部涉及到的变量从CPU缓存中移除,且要求线程必须去主内存中重新拿数据,在释放锁之后,会立即将CPU缓存中的数据同步到主内存。

4.3 Lock

4.3.1 优化代码

package com.ningzhaosheng.thread.concurrency.features.visible.lock;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @author ningzhaosheng* @date 2024/2/5 19:57:24* @description 测试Lock*/
public class TestLock {private static boolean flag = true;private static Lock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag) {lock.lock();try {//...} finally {lock.unlock();}}System.out.println("t1线程结束");});t1.start();Thread.sleep(10);flag = false;System.out.println("主线程将flag改为false");}
}

4.3.2 测试结果

4.3.3 Lock实现可见性原理分析

4.3.3.1 源码分析

通过以上截图可以看到,我们创建了一个ReentrantLock,调用了它的lock()方法,而ReentrantLock实现了Lock接口和基于AQS定义实现了锁。

4.3.3.2 总结

Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。

Lock锁是基于volatile实现的。Lock锁内部再进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。

如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从CPU缓存立即同步到主内存,同时也会将其他的属性也立即同步到主内存中。还会将其他CPU缓存行中的这个数据设置为无效,必须重新从主内存中拉取。

参考4.1.3.3 volatile原理总结部分。

4.4 final

4.4.1 final 实现可见性原理分析

final修饰的属性,在运行期间是不允许修改的,这样一来,就间接的保证了可见性,所有多线程读取final属性,值肯定是一样。

final并不是说每次取数据从主内存读取,他没有这个必要,而且final和volatile是不允许同时修饰一个属性的。

final修饰的内容已经不允许再次被写了,而volatile是保证每次读写数据去主内存读取,并且volatile会影响一定的性能,就不需要同时修饰。

好了,本次内容就分享到这,欢迎关注本博主。如果有帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/467327.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

netstat命令

netstat 是一个计算机网络命令行工具,用于显示网络连接、路由表和网络接口等网络相关信息。netstat 命令可以在各种操作系统上使用,包括 Windows、Linux 和 macOS 等。 在使用 netstat 命令时,可以提供不同的选项来显示不同类型的网络信息。…

(AtCoder Beginner Contest 334) --- F - Christmas Present 2 -- 题解

F - Christmas Present 2 F - Christmas Present 2 题目大意: 思路解析: 因为他是顺序前往每个孩子的家,前往时必须要带一个礼物,并且最多只能带k个礼物,所以它每次前往最多k个孩子之后就要回到初始点重新出发。…

哈工大计算机网络考试经验及资源分享

如果你觉得资源对你有用,在收藏的同时不要忘记点个赞(●◡●),你的支持,是我坚持创作的最佳动力。 哈工大计算机网络是一门重要的课程,对于学习计算机网络知识非常有帮助。在学习这门课程时,我选择了中科大zq老师的网…

二叉树的垂直遍历

1.题目 这道题是2024-2-13的签到题,题目难度为困难。 考察的知识点是DFS算法和自定义排序。 题目链接:二叉树的垂直遍历 给你二叉树的根结点 root ,请你设计算法计算二叉树的 垂序遍历 序列。 对位于 (row, col) 的每个结点而言&#xff…

VTK 正交投影 透视投影

1.VTK默认透视投影(近大远小); 1.调用vtkCamera的ParallelProjectionOn函数开启 2.通过vtkCamera的SetParallelScale缩放 3.通过vtkCamera的SetClippingRange设置前后裁剪平面 2.正交投影(平行投影,远近一样&#xf…

openGauss学习笔记-218 openGauss性能调优-确定性能调优范围-硬件瓶颈点分析-I/O

文章目录 openGauss学习笔记-218 openGauss性能调优-确定性能调优范围-硬件瓶颈点分析-I/O218.1 查看I/O状况218.2 性能参数分析 openGauss学习笔记-218 openGauss性能调优-确定性能调优范围-硬件瓶颈点分析-I/O 获取openGauss节点的CPU、内存、I/O和网络资源使用情况&#xf…

LeetCode:83和82.删除排序链表中的重复元素I,II

这两题算是链表的基础题,就遍历删除没啥特点, 83甚至不需要考虑第一个结点的特殊情况,属实是名副其实的easy了 LeetCode:21.合并两个有序链表之第一次的特殊情况-CSDN博客 83. 删除排序链表中的重复元素 - 力扣(Lee…

第三百二十二回

文章目录 1. 概念介绍2. 使用方法2.1 基本用法2.2 缓冲原理 3. 示例代码4. 内容总结 我们在上一章回中介绍了"FadeInImage组件"相关的内容,本章回中将介绍CachedNetworkImage组件.闲话休提,让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章…

渗透测试练习题解析 3(CTF web)

1、[网鼎杯 2020 朱雀组]phpweb 1 考点:反序列化漏洞利用 进入靶场,查看检查信息,发现存在两个参数 func 和 p 查看页面源代码 payload:funcfile_get_contents&pphp://filter/resourceindex.php 整理后,就是 PHP 代…

HiveSQL——连续增长问题

注:参考文章: SQL连续增长问题--HQL面试题35_sql判断一个列是否连续增长-CSDN博客文章浏览阅读2.6k次,点赞6次,收藏30次。目录0 需求分析1 数据准备3 小结0 需求分析假设我们有一张订单表shop_order shop_id,order_id,order_time…

基于Java (spring-boot)的职工管理系统

一、项目介绍 基于Java (spring-boot)的职工管理系统功能:登录、老板、职员人事经理、职员、部门列表、职工列表、权限修改、查看所有管理、正在审核列表、添加考勤、添加职工、添加奖惩、合同列表、合同信息、等等等。 适用人群:适合小白、大学生、毕业…

【Java八股面试系列】JVM-类和对象加载过程

目录 类和对象的加载过程 类的生命周期 类的加载过程 加载 验证 准备 解析 初始化 类卸载 对象的加载过程 类和对象的加载过程 什么是类加载和对象加载? 类加载(Class Loading):这是指JVM在运行时将类的字节码文件加载到内存中的…