Java内存模型之可见性

news2024/9/23 2:34:19

文章目录

  • 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() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public 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即可

    // writerThread
    private void change() {
        a = 3;
        b = a; // 作为刷新之前变量的触发器
    }

    // readerThread
    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() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public 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();

    @Override
    public 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();

    @Override
    public 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();

    @Override
    public 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.coloradmin.cn/o/1381077.html

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

相关文章

Kafka的核心原理

Topic的分区和副本机制 分区有什么用呢? 作用&#xff1a; 1- 避免单台服务器容量的限制: 每台服务器的磁盘存储空间是有上限。Topic分成多个Partition分区&#xff0c;可以避免单个Partition的数据大小过大&#xff0c;导致服务器无法存储。利用多台服务器的存储能力&#…

时序预测 | Matlab实现EEMD-SSA-BiLSTM、EEMD-BiLSTM、SSA-BiLSTM、BiLSTM时序预测对比

时序预测 | Matlab实现EEMD-SSA-BiLSTM、EEMD-BiLSTM、SSA-BiLSTM、BiLSTM时间序列预测对比 目录 时序预测 | Matlab实现EEMD-SSA-BiLSTM、EEMD-BiLSTM、SSA-BiLSTM、BiLSTM时间序列预测对比预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现EEMD-SSA-BiLSTM、…

HTML5+CSS3+JS小实例:音频可视化

实例:音频可视化 技术栈:HTML+CSS+JS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><m…

Rust-模式解构

match 首先&#xff0c;我们看看使用match的最简单的示例&#xff1a; exhaustive 有些时候我们不想把每种情况一一列出&#xff0c;可以用一个下划线来表达“除了列出来的那些之外的其他情况”&#xff1a; 下划线 下划线还能用在模式匹配的各种地方&#xff0c;用来表示…

【STM32】STM32学习笔记-USART串口手法HEX和文本数据包(29)

00. 目录 文章目录 00. 目录01. 串口简介02. 串口收发HEX数据包接线图03. 串口收发HEX数据包示例104. 串口收发HEX数据包示例205. 串口收发文本数据包接线图06. 串口收发文本数据包示例07. 程序示例下载08. 附录 01. 串口简介 串口通讯(Serial Communication)是一种设备间非常…

高精度彩色3D相机:开启崭新的彩色3D成像时代

3D成像的新时代 近年来&#xff0c;机器人技术的快速发展促使对3D相机技术的需求不断增加&#xff0c;原因在于&#xff0c;相机在提高机器人的性能和实现多种功能方面发挥了决定性作用。然而&#xff0c;其中许多应用所需的解决方案更复杂&#xff0c;仅提供环境的深度信息是…

【LeetCode】142. 环形链表 II(中等)——代码随想录算法训练营Day04

题目链接&#xff1a;142. 环形链表 II 题目描述 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了…

Docker 介绍 及 支持的操作系统

Docker组成&#xff1a; Docker主机(Host)&#xff1a; 一个物理机或虚拟机, 用于运行Docker服务进程和容器, 也成为宿主机, node节点。 Docker服务器端(Server)&#xff1a; Docker守护进程, 运行Docker容器。 Docker客户端(Client)&#xff1a; 客户端使用docker命令或其他工…

层叠布局(Stack)

目录 1、概述 2、开发布局 3、对齐方式 3.1、TopStart 3.2、Top 3.3、TopEnd 3.4、Start 3.5、Center 3.6、End 3.7、BottomStart 3.8、Bottom 3.9、BottomEnd 4、Z序控制 5、场景示例 1、概述 层叠布局&#xff08;StackLayout&#xff09;用于在屏幕上预留一…

Linux的SSH服务

一.SSH服务简介 1.什么是SSH SSH&#xff08;Secure Shell&#xff09;是一种安全通道协议&#xff0c;主要用来实现字符界面的远程登录、远程复制等功能。SSH 协议对通信双方的数据传输进行了加密处理&#xff0c;其中包括用户登录时输入的用户口令&#xff0c;SSH 为建立在应…

Elasticsearch:Search tutorial - 使用 Python 进行搜索 (四)

在本节中&#xff0c;你将了解另一种机器学习搜索方法&#xff0c;该方法利用 Elastic Learned Sparse EncodeR 模型或 ELSER&#xff0c;这是一种由 Elastic 训练来执行语义搜索的自然语言处理模型。这是继之前的文章 “Elasticsearch&#xff1a;Search tutorial - 使用 Pyth…

NeRF算法模型简析:从理论到实践的轻度解析以及如何编辑和微调

nerf模型可编辑的&#xff1f; NeRF模型的可编辑性&#xff08;editability&#xff09;指的是能够修改预训练的NeRF模型以改变其生成的场景或对象的某些特征&#xff0c;而不是从头开始重新训练模型。这种编辑可以是改变颜色、形状、纹理或者添加、移除和修改场景中的对象。 在…

在CentOS上设置和管理静态HTTP网站的版本控制

在CentOS上设置和管理静态HTTP网站的版本控制是一项重要的任务&#xff0c;它可以帮助您跟踪和回滚对网站所做的更改&#xff0c;确保数据的一致性和完整性。以下是在CentOS上设置和管理静态HTTP网站的版本控制的步骤&#xff1a; 安装版本控制系统在CentOS上安装Git或其他版本…

LeetCode讲解篇之78. 子集

文章目录 题目描述题解思路题解代码 题目描述 题解思路 初始化一个start变量记录当前从哪里开始遍历搜索nums 搜索过程的数字组合加入结果集 然后从start下标开始遍历nums&#xff0c;更新start&#xff0c;递归搜索 直到搜索完毕&#xff0c;返回结果集 题解代码 class …

记录用python封装的第一个小程序

前言 我要封装的是前段时间复现的一个视频融合拼接的程序&#xff0c;现在我打算将他封装成exe程序&#xff0c;我在这里只记录一下我封装的过程&#xff0c;使用的是pyinstaller&#xff0c;具体的封装知识我就不多说了&#xff0c;可以参考我另一篇博客&#xff1a;将Python…

逼格满满,推荐一个高效测试用例工具:XMind2TestCase !

一、背景 软件测试的核心是什么&#xff1f;毫无疑问是测试分析和测试用例设计&#xff0c;也是日常测试投入最多时间的工作内容之一。 然而&#xff0c;传统的测试用例设计过程有很多痛点&#xff1a; 1、使用Excel表格进行测试用例设计&#xff0c;虽然成本低&#xff0c;但…

POI:对Word的基本操作

1 向word中写入文本并设置样式 package com.example;import org.apache.poi.xwpf.usermodel.*;import java.io.File; import java.io.FileOutputStream;/*** Author&#xff1a;xiexu* Date&#xff1a;2024/1/12 23:54*/ public class WriteWord {static String PATH "…

Linux(Ubantu)交叉编译生成windows(32位,64位)可执行程序和库

机缘 机缘巧合下收到了这个小任务. 工作流 先是找了下资料发现过去的都是关于mingw32. 教程的做法: 增加个源 (trusty 是linux发行的版本标识,比如22.04是jammy deb http://us.archive.ubuntu.com/ubuntu trusty main universe更新源 sudo apt update下载mingw32. sudo…

阅读笔记lv.1

阅读笔记 sql中各种 count结论不同存储引擎计算方式区别count() 类型 责任链模式常见场景例子&#xff08;闯关游戏&#xff09; sql中各种 count 结论 innodb count(*) ≈ count(1) > count(主键id) > count(普通索引列) > count(未加索引列)myisam 有专门字段记录…

通过shell脚本确定当前平台

shell中的变量OSTYPE存储操作系统的名称&#xff0c;也可以使用uname命令来确认当前所在的平台。 shell中的变量HOSTTYPE存储操作系统的架构。 测试代码如下所示&#xff1a; #! /bin/bashecho "use OSTYPE:" if [[ "$OSTYPE" "linux-gnu&quo…