JUC并发编程15 | Java内存模型JMM与volatile

news2024/11/18 13:42:09

尚硅谷(56-70)

JMM

引入一些大厂的面试题

  1. Java内存模型JMM是什么
  2. JMM与volatile之间的关系是什么
  3. JMM有哪些特性or它的三大特性是什么
  4. 为什么要有JMM,它为什么出现?功能和作用是什么?
  5. happens-before 先行发生原则是什么

Java内存模型 Java Memory Model

CPU与内存之间的缓存关系,是线程和主内存之间的抽象关系,是一种约束和规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式,决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见。屏蔽硬件瓶盖和操作系统的内存访问差异。

原则:JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

JMM的三大特性

可见性

当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道变更。JMM规定了所有的变量都存储在主内存中。

系统主内存共享变量数据修改被写入的时机是不确定的,多线程并发下很可能出现“脏读”,所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必需在线程自己的工作内存中进行,而不能直接读写主内存中的变量。 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

线程A需要将i加1 —> 把i从主内存中读到线程A的工作内存中 —> 做+1 —> 写回主内存中

及时可见,即时通信,一旦修改就要通知其他线程。

原子性

指一个操作是不可打断的,也就是在多线程环境下,操作不能被其他线程干扰

有序性:指令重排序

对于一个线程在执行的时候,我们总认为他是从上到下有序执行。但是为了提升性能,编译器和处理器通常会对指令进行重排序。

JVM内部位置顺序话语义,即只要程序的最终结果与它的顺序化写过执行相等,那么指令执行顺序可以和代码顺序不一致,此过程叫指令重排序。JVM可以根据处理器特性,适当对机器指令进行重排序,使机器指令更符合CPU的执行特性,最大限度的发挥机器性能。

但是,在多线程中语义指令重拍是不能保证一致性的(可能会出现脏读)

代码执行示意图

源代码编译器优化重排指令并行重排内存系统的重排最终执行指令

多线程读写过程

  1. 我们定义的所有共享变量都储存在物理主内存
  2. 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  3. 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
  4. 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)

多线程先行发生原则happens-before

在JMM中,如果一个操作的执行结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必需存在 happens-before(先行发生) 原则。逻辑上的先后关系

happens-before的总原则:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
  2. 两个操作之间存在happens-before关系,并不意味着一定按照happens-before原则指定的顺序来执行。如果两个重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法

happens-before 之8条

  1. 次序规则:一个线程内,按照代码顺序,写在前面的操作**先行发生于写在后面的操作。也就是前一个操作的结果**可以被后续的操作获取。
  2. 锁定规则:一个unlock操作**先行发生于**后面(这里的“后面”指的是时间上的先后)对同一个锁的lock操作。
  3. volatile变脸规则:对一个volatile变量的写操作**先行发生于**后面对这个变量的读操作,前面的写对于后面的读是可见的。
  4. 传递规则:A先于B,B先于C,A先于C
  5. 线程启动规则(Thread Start Rule):Thread对象的start方法先行发生于此线程的每一个动作之前
  6. 线程中断规则(Thread Interruption Rule):对线程interrupt方法的调用 先行发生于 被中断线程的代码检测到中断时间的发生;可以通过Thread.interrupted()检测到是否发生中断。(也就是说要先调用interrupt方法设置过的中断标志,才能检测到中断发送
  7. 线程终止规则(Thread Termination Rule):线程中的所有操作都 先行发生于 对此线程的终止检测,我们可以通过isAlive等手段检测线程是否终止执行
  8. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造器函数执行结束)先行发生于 它的finalize方法开始(也就是先出生后死亡)

JMM小结

在Java语言里面,Happens-Before 的语义本质上是一种 可见性

A Happens-Before B 意味着A发生过的事情对B来说是可见的,无论A事件和B事件是否发生在同一个线程里。

JMM的设计分为两部分:

一部分是面向我们程序员提供的,也就是happens-before规则,它通俗易懂的向我们程序员阐述了一个强内存模型,我们只要理解happens-before规则,就可以编写并发安全的程序了。

另一部分是针对JVM实现的,为了尽可能少的对编译器和处理器做约束从而提高性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。我们只需要关注前者就好了,也就是理解happens-before规则即可,其它繁杂的内容有JMM规范结合操作系统给我们搞定,我们只写好代码即可。

private volatile int value = 0;

Volatile

volatile 两大特征

volatile的特点:可见性和有序性(排序有要求,禁止重排)。

volatile的内存语义:

  1. 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中
  2. 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量
  3. 所以volatile的写内存语义直接刷新到主内存中,读的内存语义是直接从主内存中读取
  4. 系统底层会有一个缓存行,用来缓存变量。当主内存中的共享变量被更新了,所有相关的缓存行内的数据都会被清空。

volatile 为什么可以保证可见性和有序性? —— 答:内存屏障 Memory Barrier

内存屏障**

内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步炮,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatle实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。

内存屏障之前的所有写操作都要回写到主内存

内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行

读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据

粗分:

  • 读屏障:在读指令之前插入读屏障,让工作内存或者CPU告诉缓冲区中的缓存数据失效,重新回到主内存中获取最新版
  • 写屏障:在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中
  • 全屏障:读屏障+写屏障

细分:

屏障类型指令示例说明
LoadLoadLoad1;LoadLoad;Load2保证Load1的读操作在Load2即后续读取操作之前读取
StoreStoreStore1;StoreStore;Store2在 Store2及其之后的写操作执行之前,保证Store1的写操作已经刷新到主内存
LoadStoreLoad1;LoadStore;Store2在 Store2 及其后的写操作执行前,保证Load1的操作已读取结束
StoreLoadStore1;StoreLoad;Load2在Store1的写操作已经刷新到主存后,load2及其后的读操作才能执行

保证有序性?禁重排;如何禁重排?内存重排

volatile 变量规则

当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。保证了volatile 读之后 的操作不会被重排到volatile读之前。

当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。保证了volatile 写之前 的操作不会被重排到volatile写之后。

当第一个操作为volatile写时,第二个操作是为volatile读时,都不能重排序。

volatile 特性

保证可见性

用一段代码作为示例

public class MyVolatile {
    static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println("======Thread t1 is running======");
            while (flag){

            }
            System.out.println("======flag is setetd to false======");
        },"t1").start();
        TimeUnit.SECONDS.sleep(2);
        flag = false;

        System.out.println(Thread.currentThread().getName()+"======over======");
    }
}
/**
======Thread t1 is running======
main======over======
注:程序还在运行
*/

我们在13行已经把flag改为false了,但是在t1的线程方法中,并没有将flag的值修改过来,此时t1用的还是原值

当为flag加上volatile修饰后,volatile static boolean flag = true;

/**
======Thread t1 is running======
main======over======
======flag is setetd to false======
*/

程序运行结束。

上述代码的原理解释:

线程t1中为何看不到被主线程main修改为false的flag的值?

问题可能:

  1. 主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到。
  2. 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。

我们的诉求:

  1. 线程中修改了自己工作内存中的副本之后,立即将其刷新到主内存;
  2. 工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。

解决:

使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:

  1. 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
  2. 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存

在这里插入图片描述

  1. read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
  2. load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
  3. use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
  4. assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
  5. store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
  6. write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量
    • 由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令
  7. lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
  8. unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用

没有原子性

上代码实现一下

class Sum{
    private int num = 0;
    public synchronized void add(){
        this.num++;
    }
    public int getNum(){
        return num;
    }
}
public class VolatileNoAtomic {
    public static void main(String[] args) throws InterruptedException {
        Sum sum = new Sum();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    sum.add();
                }
            },String.valueOf(i)).start();
        }
        TimeUnit.SECONDS.sleep(2);
        System.out.println("num的值是:"+sum.getNum());
    }
}
/**
num的值是:10000
*/

去掉 synchronized 后输出:num的值是:9683

加上 volatile 后输出:num的值是:8469

所以是没有原子性的。多个线程同时读了当前数据,在写回通知其他线程时,线程当前执行的++操作失效,造成数据丢失

禁止重排序

什么是重排序?

编译器未有优化程序性能而对指令序列进行重排序的一种手段,有时候会改变程序的语句的先后顺序。

  • 不存在数据依赖关系,可以重排序
  • 存在数据依赖关系,禁止重排序

注意:重排序后的指令绝对不能改变原有串行语义!

数据依赖性:若两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。

禁止重排序的三种情况:读后写,写后读,写后写。

四大屏障插入情况:

  • 在每一个volatile写操作前面插入一个StoreStore屏障:StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中
  • 在每一个volatile写操作后面插入一个StoreLoad屏障:StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
  • 在每一个volatile读操作后面插入一个LoadLoad屏障:LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
  • 在每一个volatile读操作后面插入一个LoadStore屏障:LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

用以下代码为例:

public class volatileTest {i}
    int i = 0;
    volatile boolean flag = false;
	public void write(){
        i = 2;
        flag = true;
    }
    public void read(){
        if(flag){
            System.out.println("---i =" + i);
        }
    }
}

以上代码如果不加 volatile 会存在什么问题呢?在write方法中,由于i = 2;flag = true; 之间的执行顺序是有可能被调整的,比如flag = true;此时,如果有另一个线程调用read,就可能是的输出的i为0 。而按我们的顺序执行或者预期的是2。

那么加了 volatile 之后会怎么解决呢?

在这里插入图片描述
在这里插入图片描述

使用volatile的时机

  1. 单一赋值可以,但是含有复合运算不可以。volatile x = 0;

  2. 状态标志,判断业务是否结束

  3. 低开销的读操作,写锁策略

  4. DCL双端锁的发布(在单例模式中可能获取到null值)

在这里插入图片描述

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

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

相关文章

LaTeX极简入门

​LaTeX是什么&#xff1f; LaTeX是一种基于ΤΕΧ的排版系统&#xff0c;由美国计算机学家莱斯利兰伯特&#xff08;Leslie Lamport&#xff09;在20世纪80年代初期开发。 LaTeX是一款开源免费&#xff0c;并且应用相当广泛的排版工具。不但能够对文字、公式、图片进行精确而复…

电容笔和触控笔有什么区别?电容笔牌子排行榜

而现在&#xff0c;在无纸化教育的大热之下&#xff0c;电容笔这个配件&#xff0c;也被很多人所关注。许多人对电容笔与触控笔的不同之处感到困惑&#xff0c;事实上&#xff0c;这二者是非常容易分辨的&#xff0c;电容笔是适用在我们最常见的电容屏上才能进行操作&#xff0…

算法工程师面试题

1.关于边缘提取的算法有那些&#xff1f;各有什么优缺点&#xff1f; Canny算法&#xff1a;Canny算法是一种经典的边缘检测算法&#xff0c;具有较高的准确性和良好的鲁棒性。该算法利用高斯滤波器对图像进行平滑处理&#xff0c;然后计算图像中每个像素的梯度和方向&#xff…

TinyHttpd 运行过程出现的问题

最近拉了个 TinyHttpd 的工程下来&#xff0c;不过好像各个都有些改动&#xff0c;最后挑了篇阅读量最多的。工程也是从这里面给的链接下载的。 参考自&#xff1a;https://blog.csdn.net/jcjc918/article/details/42129311 拿下来在编译运行前&#xff0c;按这里说的&#x…

词云图制作(R)

词云图制作 文章目录 词云图制作[toc]1 工作准备2 实际操作 1 工作准备 材料准备&#xff1a; 文本数据txt文件&#xff0c;或者其他文本文件。R语言软件 2 实际操作 第一步&#xff1a;从网上相关新闻网站复制粘贴或下载相关文本数据&#xff0c;转化为txt格式文件或其他&…

【设计模式】桥接模式

【设计模式】桥接模式 参考资料&#xff1a; Java 设计模式&#xff1a;实战桥接模式 一起来学设计模式之桥接模式 《设计模式之美》设计模式与范式&#xff08;结构型-桥接模式&#xff09; 桥接模式在项目中的应用 文章目录 【设计模式】桥接模式一、桥接模式概述二、案例场…

GPT-4 开始内测32k输入长度的版本了!你收到邀请了吗?

要说现在 GPT-4 最大的问题是什么&#xff1f;可能除了一时拿他没有办法的机器幻觉&#xff0c;就是卡死的输入长度了吧。尽管在一般的对话、搜索的场景里目前普通版本 GPT-4 的 8000 左右的上下文长度或许绰绰有余&#xff0c;但是在诸如内容生成、智能阅读等方面当下基础版的…

京东短网址高可用提升最佳实践 | 京东云技术团队

作者&#xff1a;京东零售 郝彦军 什么是短网址&#xff1f; 短网址&#xff0c;是在长度上比较短的网址。简单来说就是帮您把冗长的URL地址缩短成8个字符以内的短网址。 当我们在腾讯、新浪发微博时&#xff0c;有时发很长的网址连接&#xff0c;但由于微博只限制140个字&a…

Android Studio中android: baselineAligned属性认识及用途

文章目录 使用Button控件来演示使用TextView控件来演示 android:baselineAligned 设置子元素都按照基线对齐&#xff0c;默认是true 使用Button控件来演示 在项目中经常使用layout_weight属性利用比重来设置控件的大小&#xff0c;代码如下&#xff1a; <?xml version&qu…

Baumer工业相机堡盟工业相机如何使用BGAPI SDK解决两个万兆网相机的同步采集不同步的问题

Baumer工业相机堡盟工业相机如何使用BGAPI SDK解决两个万兆网相机的同步采集不同步的问题 Baumer工业相机Baumer工业相机图像数据转为Bitmap的技术背景Baumer同步异常 &#xff1a;客户使用两个Baumer万兆网相机进行同步采集发现FrameID相同&#xff0c;但是图像不同步细节原因…

2023 年第八届数维杯数学建模挑战赛 A题详细思路

下面给大家带来每个问题简要的分析&#xff0c;以方便大家提前选好题目。 A 题 河流-地下水系统水体污染研究 该问题&#xff0c;初步来看属于物理方程类题目&#xff0c;难度较大。需要我们通过查阅相关文献和资料&#xff0c;分析并建立河流-地下水系统中有机污染物的对流、…

机器学习之聚类算法一

文章目录 一、简述1. 有监督和无监督的区别&#xff0c;以及应用实例2. 为什么是聚类3. 聚类都有哪些 二、k-means1.k-means&#xff0c;核心思想是什么1. 同一个簇内的样本点相似度较高&#xff0c;这里的相似度高&#xff0c;具体指什么2.问题来了&#xff1a;同一簇之间相似…

IP-Guard能否限制PC端微信登录?

能否限制PC端微信登录&#xff1f; 不能限制微信登录&#xff0c;但可以通过应用程序控制策略&#xff0c;禁止微信程序启动。 在控制台-【策略】-【应用程序】&#xff0c;添加以下策略&#xff1a; 动作&#xff1a;禁止 应用程序&#xff1a;wechat.exe 可以实现禁止微信启…

【python 多进程】零基础也能轻松掌握的学习路线与参考资料

学习python多进程可以帮助程序员充分利用CPU的性能&#xff0c;同时提高程序的并发性和响应能力。在学习python多进程前&#xff0c;需要具备一定的Python编程基础和对操作系统进程的基本了解。 一、Python多进程学习路线 基本概念 在学习python多进程之前&#xff0c;首先需…

C++基础之默认成员函数(构造函数,析构函数)

目录 空类中都有什么 默认成员函数 构造函数 简介 特性 注意 总结 析构函数 简介 特性 注意 总结 空类中都有什么 先看下面一段代码&#xff1a; class Date {};int main() {Date d1;std::cout << sizeof(Date) << std::endl;std::cout << sizeof(d1) <…

Linux之系统基本设置(四)

1、Linux 系统基本设置 1、系统时间管理 查看系统当前时间和时区 [root192 ~]# date 2023年 05月 04日 星期四 22:43:16 EDT [root192 ~]# date -R Thu, 04 May 2023 22:43:24 -0400 [root192 ~]# date %Y %m %d %H:%M:%S 2023 05 04 22:43:38设置完整时间 [root192 ~]# da…

基于html+css的图展示67

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

Shell脚本文本三剑客之sed编辑器(拥明月入怀,揽星河入梦)

文章目录 一、sed编辑器简介二、sed工作流程三、sed命令四、sed命令的使用1.sed打印文件内容&#xff08;p&#xff09;&#xff08;1&#xff09;打印文件所有行&#xff08;2&#xff09;打印文件指定行 2.sed增加、插入、替换行&#xff08;a、i、c&#xff09;(1&#xff0…

【C++】类和对象()

&#x1f601;作者&#xff1a;日出等日落 &#x1f514;专栏&#xff1a;C 当你的希望一个个落空&#xff0c;你也要坚定&#xff0c;要沉着! —— 朗费罗 前言 面向过程和面向对象初步认识 C语言是面向过程的&#xff0c;关注…

矿井水除氟系统CH-87的技术详解

今天&#xff0c;文章中会谈到的问题是关于煤化工废水深度处理除氟、总氮、砷等污染物工艺技术的拆解分析&#xff0c;用什么样的工艺技术能把矿井水中的氟、砷、总氮做到1个毫克升以下的标准符合达标排放&#xff1f;希望能对相关行业起到一定的帮助作用。我国是一个资源丰富的…