当前位置: 首页 > news >正文

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不仅让被保护的代码安全,还近朱者赤。

相关文章:

Java内存模型之可见性

文章目录 1.什么是可见性问题2.为什么会有可见性问题3.JMM的抽象&#xff1a;主内存和本地内存3.1 什么是主内存和本地内存3.2 主内存和本地内存的关系 4.Happens-Before原则4.1 什么是Happens-Before4.2 什么不是Happens-Before4.3 Happens-Before规则有哪些4.4 演示&#xff…...

【docker】Docker Compose 使用介绍

一、什么是Docker Compose Docker Compose是一个用于定义和运行多个Docker容器的工具。它允许您使用YAML文件来配置应用程序的服务、网络和卷等方面&#xff0c;并通过单个命令即可快速启动和停止整个应用程序的多个容器。 Docker Compose的主要作用如下&#xff1a; 管理多个…...

uniapp怎么开发插件并发布

今天耳机坏了,暂时内卷不了,所以想开发几个插件玩玩,也好久没写博客了,就拿这个来写了 首先,发布插件时需要你有项目 这里先拿uniapp创建一个项目, 如下,创建好的项目长这样 然后根据uniapp官网上说的,我们发布插件时,需要在uni_modules里面编写和发布 ps:还需要使用uniapp…...

为什么不直接public,多此一举用get、set,一文给你说明白

文章目录 1. 封装性&#xff08;Encapsulation&#xff09;2. 验证与逻辑处理3. 计算属性&#xff08;Computed Properties&#xff09;4. **跟踪变化&#xff08;Change Tracking&#xff09;5. 懒加载与延迟初始化&#xff08;Lazy Initialization&#xff09;6. 兼容性与未来…...

golang 记录一次协程和协程池的使用,利用ants协程池来处理定时器导致服务全部阻塞

前言 在实习的项目中有一个地方遇到了需要协程池的地方&#xff0c;在mt推荐下使用了ants库。因此在此篇记录一下自己学习使用此库的情况。 场景描述 此服务大致是一个kafka消息接收、发送相关。接收消息&#xff0c;根据参数设置定时器进行重发。 通过这里新建kafka服务&a…...

【Postman-windows-9.12.2版本安装与汉化】

Postman-windows-9.12.2版本安装与汉化 想用英文版本的可以直接点击如下链接下载最新版本 官网最新版本(无法汉化)&#xff1a;https://www.postman.com/downloads/ 如果想要汉化的就不能使用最新版本&#xff0c;因为最新版本没有汉化包可以用 汉化包和postman的版本必须是…...

11Spring IoC注解式开发(下)(负责注入的注解/全注解开发)

1负责注入的注解 负责注入的注解&#xff0c;常见的包括四个&#xff1a; ValueAutowiredQualifierResource 1.1 Value 当属性的类型是简单类型时&#xff0c;可以使用Value注解进行注入。Value注解可以出现在属性上、setter方法上、以及构造方法的形参上, 方便起见,一般直…...

Grafana Promtail 配置解析

由于目前项目一般都是部署在k8s上&#xff0c;因此这篇文章中的配置只摘录k8s相关的配置&#xff0c;仅供参考&#xff0c;其他的配置建议上官网查询。 运行时打印配置 -print-config-stderr 通过 ./promtail 直接运行Promtail时能够快速输出配置 -log-config-reverse-order 配…...

电脑DIY-主板参数

电脑主板参数 主板系列芯片组主板支持的CPU系列主板支持CPU的第几代主板的尺寸主板支持的内存主板是否支持专用WIFI模块插槽主板规格主板供电规格M.2插槽&#xff08;固态硬盘插槽&#xff09;规格USB接口规格质保方式 华硕TUF GAMING B650M-PLUS WIFI DDR5重炮手主板 华硕&…...

JVM知识总结(持续更新)

这里写目录标题 java内存区域程序计数器虚拟机栈本地方法栈堆方法区 java内存区域 Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域&#xff1a; 程序计数器虚拟机栈本地方法栈堆方法区 程序计数器 记录下一条需要执行的虚拟机字节码指令…...

信息系统安全——基于 KALI 和 Metasploit 的渗透测试

实验 2 基于 KALI 和 Metasploit 的渗透测试 2.1 实验名称 《基于 KALI 和 Metasploit 的渗透测试》 2.2 实验目的 1 、熟悉渗透测试方法 2 、熟悉渗透测试工具 Kali 及 Metasploit 的使用 2.3 实验步骤及内容 1 、安装 Kali 系统 2 、选择 Kali 中 1-2 种攻击工具&#xff0c…...

05. 深入理解 GPT 架构

在本章的前面,我们提到了类 GPT 模型、GPT-3 和 ChatGPT 等术语。现在让我们仔细看看一般的 GPT 架构。首先,GPT 代表生成式预训练转换器,最初是在以下论文中引入的: 通过生成式预训练提高语言理解 (2018) 作者:Radford 等人,来自 OpenAI,http://cdn.openai.com/rese…...

PHP开发日志 ━━ php8.3安装与使用组件Xdebug

今天开头写点历史&#xff1a; 二十年前流行asp&#xff0c;当时用vb整合常用函数库写了一个dll给asp调用&#xff0c;并在此基础上开发一套仿windows界面的后台管理系统&#xff1b;后来asp逐渐没落&#xff0c;于是在十多年前转投php&#xff0c;不久后用php写了一套mvc框架&…...

Python - 深夜数据结构与算法之 Two-Ended BFS

目录 一.引言 二.双向 BFS 简介 1.双向遍历示例 2.搜索模版回顾 三.经典算法实战 1.Word-Ladder [127] 2.Min-Gen-Mutation [433] 四.总结 一.引言 DFS、BFS 是常见的初级搜索方式&#xff0c;为了提高搜索效率&#xff0c;衍生了剪枝、双向 BFS 以及 A* 即启发式搜索…...

langchain-Agent-工具检索

有时会定义很多工具&#xff0c;而定义Agent的时候只想使用与问题相关的工具&#xff0c;这是可以通过向量数据库来检索相关的工具&#xff0c;传递给Agent # Define which tools the agent can use to answer user queries search SerpAPIWrapper() search_tool Tool(name …...

猫头虎分享:探索TypeScript的世界 — TS基础入门 ‍

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通Golang》…...

Unity-生命周期函数

目录 生命周期函数是什么&#xff1f; 生命周期函数有哪些&#xff1f; Awake() OnEnable() Start() FixedUpdate() Update() Late Update() OnDisable() OnDestroy() Unity中生命周期函数支持继承多态吗&#xff1f; 生命周期函数是什么&#xff1f; 在Unity中&…...

SQL概述及SQL分类

SQL由IBM上世纪70年代开发出来&#xff0c;是使用关系模型的数据库应用型语言&#xff0c;与数据直接打交道。 SQL标准 SQL92,SQL99&#xff0c;他们分别代表了92年和99年颁布的SQL标准&#xff0c;我们今天使用的SQL语言依旧遵循这些标准。 SQL的分类 DDL&#xff1a;数据定…...

[VSCode] VSCode 常用快捷键

文章目录 VSCode 源代码编辑器VSCode 常用快捷键分类汇总01 编辑02 导航03 调试04 其他05 重构06 测试07 扩展08 选择09 搜索10 书签11 多光标12 代码片段13 其他 VSCode 源代码编辑器 官网&#xff1a;https://code.visualstudio.com/ 下载地址&#xff1a;https://code.visua…...

函数指针和回调函数 以及指针函数

函数指针&#xff08;Function Pointer&#xff09;&#xff1a; 定义&#xff1a; 函数指针是指向函数的指针&#xff0c;它存储了函数的地址。函数的二制制代码存放在内存四区中的代码段&#xff0c;函数的地址它在内存中的开始地址。如果把函数的地址作为参数&#xff0c;就…...

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

边缘计算医疗风险自查APP开发方案

核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

现代密码学 | 椭圆曲线密码学—附py代码

Elliptic Curve Cryptography 椭圆曲线密码学&#xff08;ECC&#xff09;是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础&#xff0c;例如椭圆曲线数字签…...

【Java_EE】Spring MVC

目录 Spring Web MVC ​编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 ​编辑参数重命名 RequestParam ​编辑​编辑传递集合 RequestParam 传递JSON数据 ​编辑RequestBody ​…...

成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战

在现代战争中&#xff0c;电磁频谱已成为继陆、海、空、天之后的 “第五维战场”&#xff0c;雷达作为电磁频谱领域的关键装备&#xff0c;其干扰与抗干扰能力的较量&#xff0c;直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器&#xff0c;凭借数字射…...

【HTTP三个基础问题】

面试官您好&#xff01;HTTP是超文本传输协议&#xff0c;是互联网上客户端和服务器之间传输超文本数据&#xff08;比如文字、图片、音频、视频等&#xff09;的核心协议&#xff0c;当前互联网应用最广泛的版本是HTTP1.1&#xff0c;它基于经典的C/S模型&#xff0c;也就是客…...

自然语言处理——循环神经网络

自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元&#xff08;GRU&#xff09;长短期记忆神经网络&#xff08;LSTM&#xff09…...

【Go语言基础【12】】指针:声明、取地址、解引用

文章目录 零、概述&#xff1a;指针 vs. 引用&#xff08;类比其他语言&#xff09;一、指针基础概念二、指针声明与初始化三、指针操作符1. &&#xff1a;取地址&#xff08;拿到内存地址&#xff09;2. *&#xff1a;解引用&#xff08;拿到值&#xff09; 四、空指针&am…...

JavaScript基础-API 和 Web API

在学习JavaScript的过程中&#xff0c;理解API&#xff08;应用程序接口&#xff09;和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能&#xff0c;使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...

GitFlow 工作模式(详解)

今天再学项目的过程中遇到使用gitflow模式管理代码&#xff0c;因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存&#xff0c;无论是github还是gittee&#xff0c;都是一种基于git去保存代码的形式&#xff0c;这样保存代码…...