Java内存模型之可见性

文章目录

  • 1.什么是可见性问题
  • 2.为什么会有可见性问题
  • 3.JMM的抽象:主内存和本地内存
    • 3.1 什么是主内存和本地内存
    • 3.2 主内存和本地内存的关系
  • 4.Happens-Before原则
    • 4.1 什么是Happens-Before
    • 4.2 什么不是Happens-Before
    • 4.3 Happens-Before规则有哪些
    • 4.4 演示:使用volatile修正可见性问题
  • 5.volatile关键字
    • 5.1 volatile是什么
    • 5.2 volatile的适用场合
    • 5.3 volatile的两点作用
    • 5.4 volatile和synchronized的关系
    • 5.5 volatile小结
  • 6.能保证可见性的措施
  • 7.升华:对synchronized可见性的正确理解

1.什么是可见性问题

首先来看第一个代码案例,演示什么是可见性问题。

/*** 演示可见性带来的问题*/
public class FieldVisibility {int a = 1;int b = 2;private void change() {a = 3;b = a;}private void print() {System.out.println("b = " + b + ", a = " + a);}public static void main(String[] args) {while (true) {FieldVisibility test = new FieldVisibility();new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}test.change();}}).start();new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}test.print();}}).start();}}
}

关于上述程序的运行结果,我们可以很容易分析得到如下三种情况:

  • b = 2, a = 3
  • b = 2, a = 1
  • b = 3, a = 3

然而,在实际运行过程中,还有可能会出现第四种情况(概率低),即 b = 3, a = 1。这是因为 a 虽然被修改了,但是其他线程不可见,而 b 恰好其他线程可见,这就造成了 b = 3, a = 1。

在这里插入图片描述

2.为什么会有可见性问题

接下来,尝试分析第二个案例。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

至此,解答一个问题:为什么会有可见性问题?

  • CPU有多级缓存,导致读的数据过期。
  • 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层。
  • 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。
  • 如果所有的核心都只用一个缓存,那么也就不存在内存可见性问题了。
  • 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。

在这里插入图片描述

3.JMM的抽象:主内存和本地内存

3.1 什么是主内存和本地内存

Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。

这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象。

在这里插入图片描述

在这里插入图片描述

3.2 主内存和本地内存的关系

JMM有以下规定:

  1. 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝。
  2. 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中。
  3. 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成。

总结:所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。

4.Happens-Before原则

4.1 什么是Happens-Before

下面的两种解释其实是一种意思。

Happens-Before规则是用来解决可见性问题的:在时间上,动作 A 发生在动作 B 之前,B 保证能看见 A,这就是Happens-Before。

两个操作可以用Happens-Before来确定它们的执行顺序:如果一个操作Happens-Before于另一个操作,那么我们说第一个操作对于第二个操作是可见的。

4.2 什么不是Happens-Before

两个线程没有相互配合的机制,所以代码 X 和 Y 的执行结果并不能保证总被对方看到的,这就不具备Happens-Before。

4.3 Happens-Before规则有哪些

(1) 单线程规则

在这里插入图片描述

(2) 锁操作(synchronized和Look)

在这里插入图片描述

在这里插入图片描述

(3) volatile变量

在这里插入图片描述

(4) 线程启动

在这里插入图片描述

(5) 线程join

在这里插入图片描述

(6) 传递性

传递性:如果 hb(A,B) 而且 hb(B,C),那么可以推出 hb(A,C)。

(7) 中断

中断:一个线程被其他线程 interrupt 时,那么检测中断(isInterrupted)或者抛出 InterruptedException 一定能看到。

(8) 构造方法

构造方法:对象构造方法的最后一行指令Happens-Before于 finalize() 方法的第一行指令。

(9) 工具类的Happens-Before原则

  • 线程安全的容器get一定能看到在此之前的put等存入动作
  • CountDownLatch
  • Semaphore
  • Future
  • 线程池
  • CyclicBarrier

4.4 演示:使用volatile修正可见性问题

Happens-Before有一个原则是:如果 A 是对 volatile 变量的写操作,B 是对同一个变量的读操作,那么 hb(A,B)。

根据上面的原则,可以使用 volatile 关键字解决本文开头第一个案例的可见性问题。

/*** 使用volatile关键字解决可见性问题*/public class FieldVisibility {int a = 1;volatile int b = 2; // 只给b加volatile即可// writerThreadprivate void change() {a = 3;b = a; // 作为刷新之前变量的触发器}// readerThreadprivate void print() {System.out.println("b = " + b + ", a = " + a);}public static void main(String[] args) {while (true) {FieldVisibility test = new FieldVisibility();new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}test.change();}}).start();new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}test.print();}}).start();}}
}

这里体现了 volatile 的一个很重要的功能:近朱者赤。给 b 加了 volatile,不仅 b 被影响,也可以实现轻量级同步。

b 之前的写入(对应代码b=a)对读取 b 后的代码(print b)都可见,所以在 writerThread 里对 a 的赋值,一定会对 readerThread 里的读取可见,所以这里的 a 即使不加 volatile,只要 b 读到是 3,就可以由Happens-Before原则保证了读取到的都是 3 而不可能读取到 1。

5.volatile关键字

5.1 volatile是什么

volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。

如果一个变量被修饰成volatile,那么JVM就知道了这个变量可能会被并发修改。

但是开销小,相应的能力也小,虽然说volatile是用来同步地保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。

5.2 volatile的适用场合

(1) 不适用于a++

import java.util.concurrent.atomic.AtomicInteger;/*** volatile的不适用场景*/
public class NoVolatile implements Runnable {volatile int a;AtomicInteger realA = new AtomicInteger();@Overridepublic void run() {for (int i = 0; i < 10000; i++) {a++;realA.incrementAndGet();}}public static void main(String[] args) throws InterruptedException {Runnable r = new NoVolatile();Thread thread1 = new Thread(r);Thread thread2 = new Thread(r);thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(((NoVolatile) r).a);System.out.println(((NoVolatile) r).realA.get());}
}

在这里插入图片描述

(2) 适用场景一

如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。例如,boolean flag 操作。

注意:volatile 适用的关键并不在于 boolean 类型,而在于和之前的状态是否有关系。

在下面的程序中,setDone() 的时候,done 变量只是被赋值,而没有其他的操作,所以是线程安全的。

import java.util.concurrent.atomic.AtomicInteger;/*** volatile的适用场景*/
public class UseVolatile implements Runnable {volatile boolean done = false;AtomicInteger realA = new AtomicInteger();@Overridepublic void run() {for (int i = 0; i < 10000; i++) {setDone();realA.incrementAndGet();}}private void setDone() {done = true;}public static void main(String[] args) throws InterruptedException {Runnable r = new UseVolatile();Thread thread1 = new Thread(r);Thread thread2 = new Thread(r);thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(((UseVolatile) r).done);System.out.println(((UseVolatile) r).realA.get());}
}

在这里插入图片描述

在下面的程序中,虽然 done 变量是 boolean 类型的,但 flipDone() 的时候,done 变量取决于之前的状态,所以是线程不安全的。

import java.util.concurrent.atomic.AtomicInteger;/*** volatile的不适用场景*/
public class NoUseVolatile implements Runnable {volatile boolean done = false;AtomicInteger realA = new AtomicInteger();@Overridepublic void run() {for (int i = 0; i < 10000; i++) {flipDone();realA.incrementAndGet();}}private void flipDone() {done = !done;}public static void main(String[] args) throws InterruptedException {Runnable r = new NoUseVolatile();Thread thread1 = new Thread(r);Thread thread2 = new Thread(r);thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(((NoUseVolatile) r).done);System.out.println(((NoUseVolatile) r).realA.get());}
}

在这里插入图片描述

(3) 适用场景二

作为刷新之前变量的触发器。

在这里插入图片描述

5.3 volatile的两点作用

可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存。

禁止指令重排序优化:解决单例双重锁乱序问题。

5.4 volatile和synchronized的关系

volatile在这方面可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。

5.5 volatile小结

  1. volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如 boolean flag 或者作为触发器,实现轻量级同步。
  2. volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
  3. volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
  4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile属性不会被线程缓存,始终从主存中读取。
  5. volatile提供了happens-before保证,对volatile变量 v 的写入happens-before所有其他线程后续对 v 的读操作。
  6. volatile可以使得long和double的赋值是原子的。关于long和double的原子性,可以参考这篇文章。

6.能保证可见性的措施

除了volatile可以让变量保证可见性外,synchronized、Lock、并发集合、Thread.join() 和 Thread.start() 等都可以保证可见性。

具体看上述happens-before原则的规定。

7.升华:对synchronized可见性的正确理解

synchronized不仅保证了原子性,还保证了可见性。

synchronized不仅让被保护的代码安全,还近朱者赤。

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

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

相关文章

【SQL注入】SQLMAP v1.7.11.1 汉化版

下载链接 【SQL注入】SQLMAP v1.7.11.1 汉化版 简介 SQLMAP是一款开源的自动化SQL注入工具&#xff0c;用于扫描和利用Web应用程序中的SQL注入漏洞。它在安全测试领域被广泛应用&#xff0c;可用于检测和利用SQL注入漏洞&#xff0c;以验证应用程序的安全性。 SQL注入是一种…

调试(c语言)

前言&#xff1a; 我们在写程序的时候可能多多少少都会出现一些bug&#xff0c;使我们的程序不能正常运行&#xff0c;所以为了更快更好的找到并修复bug&#xff0c;使这些问题迎刃而解&#xff0c;学习好如何调试代码是每个学习编程的人所必备的技能。 1. 什么是bug&#xf…

Java项目:06 Springboot的进销存管理系统

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 进销存管理系统 介绍 进销存系统是为了对企业生产经营中进货、出货、批发销售、付款等全程进行&#xff08;从接获订单合同开 始&#xff0c;进入物料采购、入…

浅析Linux进程地址空间

前言 现代处理器基本都支持虚拟内存管理&#xff0c;在开启虚存管理时&#xff0c;程序只能访问到虚拟地址&#xff0c;处理器的内存管理单元&#xff08;MMU&#xff09;会自动完成虚拟地址到物理地址的转换。基于虚拟内存机制&#xff0c;操作系统可以为每个运行中的进程创建…

ros2+gazebo(ign)激光雷达+摄像头模拟

虽然ign不能模拟雷达&#xff0c;但是摄线头是可以模拟的。 好了现在都不用模拟了&#xff0c;ign摄线头也模拟不了。 ros2ign gazebo无法全部模拟摄线头和雷达。 只能有这样2个解决方法&#xff1a; 方法1&#xff1a;使用ros2 gazebo11 方案2&#xff1a;使用ros2买一个实…

【问题探讨】基于非支配排序的蜣螂优化算法NSDBO求解微电网多目标优化调度研究

目录 主要内容 模型研究 结果一览 下载链接 主要内容 该模型以环境保护成本和运行成本为双目标构建了微电网优化调度模型&#xff0c;模型目标函数和约束条件复现文献《基于改进粒子群算法的微电网多目标优化调度》&#xff0c;程序的特点是采用非支配排序的蜣螂…

Redis缓存使用问题

数据一致性 只要使用到缓存,无论是本地内存做缓存还是使用 redis 做缓存,那么就会存在数据同步的问题。 以 Tomcat 向 MySQL 中写入和删改数据为例,来解释数据的增删改操作具体是如何进行的。 我们分析一下几种解决方案, 1、先更新缓存,再更新数据库 2、先更新数据库,…

2023年第十四届中国数据库技术大会(DTCC2023):核心内容与学习收获(附大会核心PPT下载)

随着信息化时代的深入发展&#xff0c;数据库技术作为支撑信息化应用的核心技术&#xff0c;其重要性日益凸显。本次大会以“数据价值&#xff0c;驱动未来”为主题&#xff0c;聚焦数据库领域的前沿技术与最新动态&#xff0c;吸引了数千名业界专家、企业代表和数据库技术爱好…

橘子学Spring01之spring的那些工厂和门面使用

一、Spring的工厂体系 我们先来说一下spring的工厂体系(也称之为容器)&#xff0c;得益于大佬们对于单一职责模式的坚决贯彻&#xff0c;在十几年以来spring的发展路上&#xff0c;扩展出来大量的工厂类&#xff0c;每一个工厂类都承担着自己的功能(其实就是有对应的方法实现)…

阿里云云服务器u1实例和e实例有什么区别?

阿里云服务器u1和e实例有什么区别&#xff1f;ECS通用算力型u1实例是企业级独享型云服务器&#xff0c;ECS经济型e实例是共享型云服务器&#xff0c;所以相比较e实例&#xff0c;云服务器u1性能更好一些。e实例为共享型云服务器&#xff0c;共享型实例采用非绑定CPU调度模式&am…

监督学习 - 岭回归(Ridge Regression)

什么是机器学习 岭回归&#xff08;Ridge Regression&#xff09;是一种线性回归的扩展&#xff0c;它通过在损失函数中添加正则化项&#xff08;L2范数&#xff09;来解决线性回归中可能存在的过拟合问题。正则化项有助于限制模型的参数&#xff0c;使其不过分依赖于训练数据…

LeetCode264. 丑数 II(相关话题:多重指针动态规划)

题目描述 给你一个整数 n &#xff0c;请你找出并返回第 n 个 丑数 。丑数 就是质因子只包含 2、3 和 5 的正整数。 示例 1&#xff1a; 输入&#xff1a;n 10 输出&#xff1a;12 解释&#xff1a;[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。示例 2&am…

【Sharding-Sphere 整合SpringBoot】

Sharding-Jdbc在3.0后改名为Sharding-Sphere。Sharding-Sphere相关资料&#xff0c;请自行网上查阅&#xff0c;这里仅仅介绍了实战相关内容&#xff0c;算是抛砖引玉。 Sharding-Sphere 整合SpringBoot 一、项目准备二、项目实战1. pom.xml及application.yml2. OrderInfoCont…

串行Nor Flash的结构和参数特性

文章目录 引言1、Nor Flash的结构2、Nor Flash的类别3.标准Serial Nor Flash的特征属性1.Wide Range VCC Flash2.Permanent Lock3.Default Lock Protection4.Standard Serial Interface5.Multi-I/O6.Multi-I/O Duplex (DTR)7.XIP&#xff08;片上执行&#xff09; 4.标准Serial…

Java内置锁:深度解析Lock接口中lock方法和lockInterruptibly方法

Java11中的Lock接口提供lock()和lockInterruptibly()两种锁定方法&#xff0c;用于获取锁&#xff0c;但处理线程中断时有所不同&#xff0c;lock()使线程等待直到锁释放&#xff0c;期间无视中断&#xff1b;而lockInterruptibly()在等待中若收到中断请求&#xff0c;会立即响…

倍福PLC控制器开发环境介绍

倍福PLC控制器是一款功能强大、易于使用的可编程逻辑控制器&#xff0c;广泛应用于各种工业自动化控制系统中。为了充分发挥倍福PLC控制器的功能&#xff0c;需要使用合适的开发环境。下面将介绍倍福PLC控制器的开发环境&#xff0c;主要包括软件安装与配置、工程创建与管理、编…

“超人练习法”系列08:ZPD 理论

01 先认识一个靓仔 看过 Lev Vygotsky 这个人的书吗&#xff1f;他是一位熟练心理学家&#xff0c;对人们习得技能的方式非常感兴趣&#xff0c;但他 37 岁的时候就因肺炎英年早逝了。 他认为社会环境对学习有关键性的作用&#xff0c;认为社会因素与个人因素的整合促成了学习…

element ui el-table展示列表,结合分页+过滤功能

vueelement-ui实现的列表展示&#xff0c;列表分页&#xff0c;列表筛选功能 1&#xff0c;分页器 el-table模块下面是分页器代码 <el-pagination></el-pagination> <el-table></el-table> <!-- 分页器 --><div class"block" st…

力扣每日一练(24-1-13)

如果用列表生成式&#xff0c;可以满足输出的型式&#xff0c;但是不满足题意&#xff1a; nums[:] [i for i in nums if i ! val]return len(nums) 题意要求是&#xff1a; 你需要原地修改数组&#xff0c;并且只使用O(1)的额外空间。这意味着我们不能创建新的列表&#xff…

【QT】标准对话框

目录 1 概述 2 QFileDialog对话框 1.选择打开一个文件 2.选择打开多个文件 3&#xff0e;选择已有目录 4&#xff0e;选择保存文件名 3 QColorDialog对话框 4 QFontDialog对话框 5 QInputDialog标准输入对话框 1.输入文字 2&#xff0e;输入整数 3&#xff0e;输入…