JAVA内存模型(JMM)

news2024/11/25 14:24:02

JMM

    • 一、JMM——原子性-(synchronized)
    • 二、JMM——可见性-问题
      • 2.1 退不出的循环
      • 2.2 可见性——解决
      • 2.3 可见性
    • 三、JMM——有序性-问题
      • 3.1 诡异的结果
      • 3.2 解决方法
      • 3.3 有序性理解
      • 3.4 happens-before

JMM 即 Java Memory Model,简单地说,JMM定义了一套在多线程读写共享数据(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障。它定义了主存(共享内存)、工作内存(线程私有)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。

一、JMM——原子性-(synchronized)

语法

synchronized(对象){
要作为原子操作代码
}

用synchronized解决并发问题

public class Demo {

   static int i=0;
   static Object obj=new Object();
   
    public static void main(String[] args) throws InterruptedException {

      Thread t1=new Thread(()->{
          for (int i= 0; i<50000 ; i++) {
              synchronized (obj) {
                  i++;
              }
          }
      });
      Thread t2=new Thread(()->{
          for (int j = 0; j <50000 ; j++) {
              synchronized (obj) {
                  i--;
              }
          }
      });
      t1.start();
      t2.start();
      // join: 让主线程等待,一直等到其他线程不再活动为止 
      t1.join();
      t2.join();
        System.out.println(i);
    }
}

建议用synchronized对对象加锁的力度稍微大些(上述写法需对对象锁进行50000操作,较耗时),可将代码做如下调整:
在这里插入图片描述
如何理解呢:可以将obj想象成一个房间, 线程t1, t2想象成两个人。

当线程t1执行到synchronized(obj)时就好比t1进入了这个房间,并反手锁住了门,在门内执行count++代码。

这时候如果t2也运行到了synchronized(obj)时,它发现i ]被锁住了,只能在i门外等待。

当tl执行完synchronized{}块内的代码,这时候才会解开门上的锁,从obj房间出来。t2 线程这时才可以进入obj房间,反锁住门,执行它的count–代码。

上例中tl和t2线程必须用synchronized锁住同-个obj对象,如果1锁住的是ml对象,t2 锁住的是m2对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果

实际开发中,具体业务具体分析,如果业务很短多应使锁的粒度应该尽可能的小,这样便可缩短其他线程等待时间,以敬可能的提高并发效率

JMM——原子性-问题

问题提出:两个线程对初始值为0的静态变量,一个做自增,一个做自减,各做5000次,结果是0吗?
在这里插入图片描述

以上的结果可能是正数、负数、零。【Java中对静态变量的自增,自减并不是原子操作;在多线程环境下这些操作可能会被CPU进行交错执行】
例如相对于i++而言(i为静态变量)====>静态变量的i++需要将静态变量和常数都放在操作数栈用iadd来完成自增,并非在局部变量表中直接执行 ,实际产生如下的JVM指令:

getsatic     i      // 获取静态常量i的值
iconst        i      // 准备常量1
iadd                 // 加法【局部变量自增调用iinc】
putstatic    i     // 将修改后的值存入静态变量i

对应i–也是类似:

getsatic     i      // 获取静态常量i的值
iconst        i      // 准备常量1
isub                 // 加法
putstatic    i     // 将修改后的值存入静态变量i

而Java的内存模型分为如下两部分(主内存工作内存),完成静态变量的自增,自减需要在主存和线程内存中进行数据交换:
在这里插入图片描述
静态变量(共享的变量)放在主存中;线程则是在工作内存中

若为单线程以上8行代码是顺序执行(不会交错)没有问题:
在这里插入图片描述
但多线程下这8行代码可能交错运行
操作系统的线程模型都是一种抢先多任务模型,线程会轮流拿到CPU的使用权;CPU会以时间片为单位,在时间片1把使用权交给线程1运行,在时间片2再把线程分给线程2运行===>多个线程轮流使用CPU

出现负数的情况:【第一个线程执行getstatic获取到静态变量的初始值(i=0),恰巧在此时刻时间排片被用完,CPU将其剔出,于是CPU开始执行线程2的代码…】
在这里插入图片描述

出现正数的情况:
在这里插入图片描述

二、JMM——可见性-问题

2.1 退不出的循环

观察现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止(程序始终不能停止下来)
1在这里插入图片描述
分析原因如下:

初始状态,t线程刚开始从主内存读取了run的值到工作内存
在这里插入图片描述

因为t线程要频繁从内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中(做进一步优化),减少对主存中run的访问,提高效率
在这里插入图片描述

1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
在这里插入图片描述

2.2 可见性——解决

volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中的高速缓存中查找变量的值,必须到主存中获取它的值,线程操做volatile变量都是直接操作主存===>volatile修饰的变量每次都到主存中读取

2.3 可见性

上述例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile 变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程, 多个读线程的情况:

上例从字节码理解是这样的:
在这里插入图片描述

加上volatile关键字后,程序运行1s后便可结束
在这里插入图片描述

比较之前线程安全时所举的例子:两个线程一个i++一个i–(不可保证原子性)
在这里插入图片描述

注意:synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低

如果在前面示例的死循环中加入System. out.printla()会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了,原因是什么?
在这里插入图片描述

在这里插入图片描述
查看println()方法源码后发现具有synchronized关键字,要对当前打印输出流做同步

三、JMM——有序性-问题

3.1 诡异的结果

  int num=0;
  boolean ready=false;

    // 线程1执行此方法
    public void actor1(I_Result r){
        if(ready){
            r.r1=num+num;
        }else {
            r.r1=1;
        }
}

    // 线程2执行此方法
    public void actor2(I_Result r){
        num=2;
        ready=true;
    }

I Result是一个对象,有一个属性r1用来保存结果,问,可能的结果有几种?

情况1: 线程1先执行,这时ready= false,所以进入else 分支结果为1
情况2: 线程2先执行num=2,但没来得及执行ready= true,线程1执行,还是进入else分支,结果为1
情况3: 线程2执行到ready=true,线程1执行,这回进入if分支,结果为4 (因为num已经执行过了)

除以上情况,结果还有可能为0: 线程2执行ready=true(num=2的赋值有可能还没执行),切换到线程1,进入if分支,相加为0,再切回线程2执行num=2

这种现象在Java内存模型中称为指令重排,是JIT编译器在运行时的一些优化,这个现象需要通过大量测试才能发现:可借助java并发压测工具jcstresshttps://wiki.openjdk.java.net/display/CodeTools/jcstress

3.2 解决方法

volatile修饰的变量,可以禁用指令重排

3.3 有序性理解

同一个线程内,JVM会在不影响正确性的前提下,可以调整语句的执行顺序,思考以下代码

static   int  i
static   int  j

// 在某个线程内执行如下赋值操作
i = ...;    // 较为耗时的操作(假如i的赋值可能需要做一些计算)
j = ...;

假如i的赋值可能需要做一些计算,j的可能马上会运算完毕;这种情况下JVM会对指令进行调整。因此以上操作无论是先执行i还是先执行j,对最终的结果不会产生影响。所以,上面代码真正执行时,即可以是

i = ...;    // 较为耗时的操作
j = ...;

也可以是

j = ...;
i = ...;    // 较为耗时的操作

这种特性称之为【指令重排】,多线程下指令重排会影响正确性,例如著名的double-checked locking模式实现单例

实现要点:单例类且为懒惰初始化===>单例是否创建,若没有创建则new单例对象;若已创建,则直接拿到上次已创建好的单例对象(为实现懒惰初始化也应考虑线程安全问题)

public final class Singleton {

    private Singleton() {
    }

    private static Singleton INSTANCE = null;

    public static Singleton getINSTANCE() {
        // 实例没创建,才会进入内部的synchronized代码块
        if (INSTANCE == null){
            synchronized (Singleton.class){
                //也许有其他线程已经创建实例,所以再判断一次
                if (INSTANCE == null){
                    INSTANCE=new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

以上的实现特点:
● 懒惰实例化
● 首次使用getINSTANCE()才使用synchronized加锁,后续使用无需加锁

但在多线程下,上面的代码存在问题(发生指令重排), INSTANCE=new Singleton();对应的字节码为:
在这里插入图片描述
其中4、7两个步骤时不固定的,也许jvm会优化为:先将引用地址赋值给INSTANCE变量后,再执行构造方法,如果两个线程t1、t2按如下时间顺序执行:
在这里插入图片描述
这时t1还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2拿到的将是一个未初始化完毕的单例

对INSTANCE使用volatile修饰即可,可以禁用指令重排,但要注意JDK5以上版本的volatile才会真正有效

3.4 happens-before

happens-before规定了哪些写操作对其他线程的读操作可见,他是可见性与有序性的一套规则总结:

线程解锁m之前对变量的锁,对于接下来对m加锁的其他线程对该变量的读可见

static int i=0;
static Object obj=new Object();

new Thread(()->{
              synchronized (m) {
                 x = 10;
              }
},"t1").start();

new Thread(()->{
              synchronized (m) {
                System.out.println(x);
              }
},"t2").start();

线程对volatile变量的写,对接下来其他线程对该变量的读可见

volatile  static int  x;

new Thread(()->{
      x = 10; 
},"t1").start();

new Thread(()->{
      System.out.println(x);
},"t2").start();

线程start前对变量的写,对该线程开始后对该变量的读可见

static int  x;
x = 10;

new Thread(()->{
      System.out.println(x);
},"t2").start();

线程结束前对变量的写,对其他线程得知它结束后的读可见(比如其他线程调用t1.isAlive()或t1.join()等待它结束)

static int  x;

 Thread t1=new Thread(()->{
        x = 10;
 },"t1");
 t1.start();

t1.join()  // 主线程调用t1.join()等待t1结束
System.out.println(x);

线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupt或t2.isInterrupted)

public final class JMM {

    static int x;

    public static void main(String[] args) throws InterruptedException {

        Thread t2 = new Thread(() -> {
            while (true) {
                // 与主线程类似,在被打断后继续循环,并不会影响线程的继续运行(设置一个线程的打断标记)
                // 被打断后下次再进入循环条件成立
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(x);
                    break;
                }
            }
        }, "t2");
        t2.start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // x的写操作是在打断前写的,因此得知它打断后再读取x的值必定可以拿到x最新的结果
            x = 10;
            // 打断t2线程(1s后)
            t2.interrupt();
        }, "t1").start();

        // 主线程不断循环,观察t2是否被打断
        while (!t2.isInterrupted()) {
            // 若未打断则一直循环       
            Thread.yield();
        }
        System.out.println(x);
    }
}

● 对变量默认值(0,false,null)的写,对其他线程对该变量的读可见

● 具有传递性,如果x hb->y并且y hb->z那么有x hb->z
变量都是指成员变量或静态成员变量

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

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

相关文章

JVM - 字节码执行引擎

目录 栈帧和局部变量表 概述 栈帧概述 栈帧概念结构 局部变量表 slot是复用的实例 操作数栈 概述 动态连接 方法调用 静态分派和动态分派 栈帧和局部变量表 概述 JVM的字节码执行引擎&#xff0c;功能基本就是输入字节码文件&#xff0c;然后对字节码进行解析并处理…

jdk19下载与安装教程(win10)超详细

一、下载安装步骤 1、官网下载还需要注册&#xff0c;可以点【我的网盘】目录下载&#xff0c;目录也有其它低版本的&#xff0c;如果有需要大家根据需要自行选择。 2、下载后直接点击安装程序&#xff0c;点击【运行】。这里我使用的是64位的。 3、点击【下一步】。 4、默认安…

28-Golang中的数组

Golang中的数组数组介绍数组的定义和内存布局数组的定义数组的内存图数组 的使用访问数组元素案例四种初始化的方式for-range结构遍历基本语法说明案例数组使用注意事项和细节数组应用案例1.创建一个byte类型的26个元素的数组&#xff0c;分别放置A-Z。使用for循环访问所有元素…

分享SEO优化的8个技巧

什么是SEO? SEO是Search Engine Optimization的缩写&#xff0c;直译过来就是“搜索引擎优化”的意识。故名意思&#xff0c;SEO是一种优化&#xff08;提高&#xff09;网站在搜索引擎内的自然排名的行为的统称。 当里个当&#xff0c;当里个当&#xff0c;闲言碎语不要讲&a…

Web应用程序自动化测试工具Selenium的主要功能有哪些?

Selenium是一个用于Web应用程序测试的工具。是一个开源的Web的自动化测试工具&#xff0c;最初是为网站自动化测试而开发的&#xff0c;类型像我们玩游戏用的按键精灵&#xff0c;可以按指定的命令自动操作&#xff0c;不同是Selenium可以直接运行在浏览器上&#xff0c;它支持…

VSCode最新版本下载安装详细教程(win10)

VSCode是Microsoft发布的一款运行于多个操作系统&#xff0c;针对于编写现代Web和云应用的跨平台的源代码编辑器&#xff0c; 可在桌面上运。它具有对JavaScript&#xff0c;TypeScript和Node.js的内置支持&#xff0c;并具有丰富的其他语言&#xff08;例如C&#xff0c;C&…

vue-echarts实现多功能图表

前言作为前端人员&#xff0c;日常图表、报表、地图的接触可谓相当频繁&#xff0c;今天小编隆重退出前端框架之VUE结合百度echart实现中国地图各种图表的展示与使用&#xff1b;作为“你值得拥有”专栏阶段性末篇&#xff0c;值得一看主要实现功能——中国地图——环形图——折…

Springboot扩展点之DisposableBean

前言DisposableBean&#xff0c;是在Spring容器关闭的时候预留的一个扩展点&#xff0c;从业务开发的角度来看&#xff0c;基本上是用不到的&#xff0c;但是Spring容器从启动到关闭&#xff0c;是Spring Bean生命周期里一个绕不开的节点&#xff0c;因此还是有必要学习一下&am…

Web3中文|关于以太坊“上海升级”,你需要知道哪些?

今年3月&#xff0c;以太坊将进行自2022年9月转向权益证明系统以来的首次大升级&#xff0c;即上海硬交叉。一旦以太坊完成“上海升级”&#xff0c;帮助运营网络的验证者将能够提取1600万枚被质押的以太币&#xff08;ETH&#xff09;。 除了重点落实以太坊改进建议——4895&…

吉林电视台启用乾元通多卡聚合系统广电视频传输解决方案

随着广播电视数字化、IP化、智能化的逐步深入&#xff0c;吉林电视台对技术改造、数字设备升级提出了更高要求&#xff0c;通过对系统性能、设计理念的综合评估&#xff0c;正式启用乾元通多卡聚合系统广电视频传输解决方案&#xff0c;将用于大型集会、大型演出、基层直播活动…

idea使用本地代码远程调试线上运行代码---linux环境

场景&#xff1a; 之前介绍过windows环境上&#xff0c;用idea进行远程调试那么在linux环境下实战一下 环境&#xff1a; linux 测试应用&#xff1a;使用docker部署的platform-multiappcenter-base-app-1.0.0-SNAPSHOT.jar 应用 测试应用端口&#xff1a;19001 测试工具&…

工欲善其事,必先利其器,分享5款Windows效率软件

工欲善其事&#xff0c;必先利其器。作为全球最多人使用的桌面操作系统&#xff0c;Windows 的使用效率与我们的工作学习息息相关。今天&#xff0c;小编就为大家整理了5款提高效率的利器&#xff0c;让你的 Windows 更具生产力。 1.桌面自定义——Rainmeter Rainmeter是一款…

快速部署私有云笔记,免费享受多端同步

一、老Q笔记之一路坎坷 市面上的笔记软件非常多&#xff0c;有些是本地编辑功能特别强大但是不支持云同步&#xff0c;有些是支持上云但是编辑功能不够完善。选择一款合适的云笔记软件&#xff0c;无疑能让我们工、学习的时候更加顺心、顺手。 这么多年来老Q使用过很多云笔记…

亚马逊云科技与CIT强强联手,因企制宜加速数字化进程

数字经济时代&#xff0c;数据逐渐成为企业重要的生产要素&#xff0c;并成为驱动生产力增长的助力。但数据的快速增长&#xff0c;也给企业带来了诸多挑战&#xff0c;如&#xff1a;企业将彻底改变内外部流程、数据量超越了传统数据库的管理能力等。 作为亚马逊云科技全球咨…

10、创建和管理表

文章目录1 基础知识1.1 一条数据存储的过程1.2 标识符命名规则1.3 MySQL中的数据类型2. 创建和管理数据库2.1 创建数据库2.2 使用数据库2.3 修改数据库2.4 删除数据库3. 创建表3.1 创建方式13.2 创建方式23.3 查看数据表结构4 修改表4.1 追加一个列4.2 修改一个列4.3 重命名一个…

yolov5使用

参考网址&#xff1a;https://zhuanlan.zhihu.com/p/501798155 源码下载及使用 release下载source及pt文件&#xff08;yolov5s.pt&#xff09; https://github.com/ultralytics/yolov5/tags https://github.com/ultralytics/yolov5/releases/tag/v5.0 安装yolov5训练所需的第…

433MHz无线通信--模块RXB90

1、接收模块RXB90简介 两个数据输出是联通的。 2、自定义一个编码解码规则 组数据为“0x88 0x03 0xBD 0xB6”。 3、发射模块 如何使用示波器得到捕捉一个周期的图像&#xff1f; 通过date引脚连接示波器CH1&#xff0c;以及示波器探针的接地端接芯片的GND&#xff0c;分…

初识C语言——函数

目录 一、库函数 二、自定义函数 三、函数的参数 四 、函数的调用 1 、传值调用 2 、传址调用 五、函数的嵌套调用和链式访问 六、函数的声明和定义 1 函数声明&#xff1a; 2 函数定义&#xff1a; 七、函数的递归与迭代 八、总结 一、库函数 库函数查询网站&#xff…

浅谈智慧城市管廊综合管理平台的建设

摘 要&#xff1a;随着智慧城市的发展&#xff0c;地下综合管廊的建设不断增多&#xff0c;建成后的管廊需要有科学合理的综合管理平台对其进行智能化管理。本文介绍了地下综合管廊的建设内容&#xff0c;从管廊智能化管理角度出发&#xff0c;在运用GIS、可视化、传感器、物联…

多线程服务器

设计一个客户端从服务器端获取时间的程序&#xff1a; 服务器端使用多线程的方式&#xff0c;当有客户端请求到达时&#xff0c;服务器将启动一个新线程为它返回当前的时间&#xff0c;服务完后线程自动销毁&#xff0c;服务器端会显示连接的次数。 客户端比较简单&#xff0c;…