当前位置: 首页 > 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;就…...

浅谈 React Hooks

React Hooks 是 React 16.8 引入的一组 API&#xff0c;用于在函数组件中使用 state 和其他 React 特性&#xff08;例如生命周期方法、context 等&#xff09;。Hooks 通过简洁的函数接口&#xff0c;解决了状态与 UI 的高度解耦&#xff0c;通过函数式编程范式实现更灵活 Rea…...

地震勘探——干扰波识别、井中地震时距曲线特点

目录 干扰波识别反射波地震勘探的干扰波 井中地震时距曲线特点 干扰波识别 有效波&#xff1a;可以用来解决所提出的地质任务的波&#xff1b;干扰波&#xff1a;所有妨碍辨认、追踪有效波的其他波。 地震勘探中&#xff0c;有效波和干扰波是相对的。例如&#xff0c;在反射波…...

可靠性+灵活性:电力载波技术在楼宇自控中的核心价值

可靠性灵活性&#xff1a;电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中&#xff0c;电力载波技术&#xff08;PLC&#xff09;凭借其独特的优势&#xff0c;正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据&#xff0c;无需额外布…...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

从零实现STL哈希容器:unordered_map/unordered_set封装详解

本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说&#xff0c;直接开始吧&#xff01; 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)

🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

select、poll、epoll 与 Reactor 模式

在高并发网络编程领域&#xff0c;高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表&#xff0c;以及基于它们实现的 Reactor 模式&#xff0c;为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。​ 一、I…...

selenium学习实战【Python爬虫】

selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...

【笔记】WSL 中 Rust 安装与测试完整记录

#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统&#xff1a;Ubuntu 24.04 LTS (WSL2)架构&#xff1a;x86_64 (GNU/Linux)Rust 版本&#xff1a;rustc 1.87.0 (2025-05-09)Cargo 版本&#xff1a;cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...

Git常用命令完全指南:从入门到精通

Git常用命令完全指南&#xff1a;从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...