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

JMM(Java内存模型)

定义

JMM即Java内存模型(Java memory model),在JSR133里指出了JMM是用来定义一个一致的、跨平台的内存模型,是缓存一致性协议,用来定义数据读写的规则。

内存可见性

在Java中,不同线程拥有各自的私有工作内存,当线程需要读取或修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到线程的工作内存变量副本中,当该线程修改其变量副本的值后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值

指令重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,指令重排序使得代码在多线程执行时会出现一些问题。

其中最著名的案例便是在初始化单例时由于可见性重排序导致的错误。

单例模式

案例1
 
public class Singleton {
    private static Singleton singleton;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

以上代码是经典的懒汉式单例实现,但在多线程的情况下,多个线程有可能会同时进入if (singleton == null) ,从而执行了多次singleton = new Singleton(),从而破坏单例。

案例2
 
public class Singleton {
    private static Singleton singleton;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

以上代码在检测到singleton为null后,会在同步块中再次判断,可以保证同一时间只有一个线程可以初始化单例。但仍然存在问题,原因就是Java中singleton = new Singleton()语句并不是一个原子指令,而是由三步组成:

  1. 为对象分配内存
  2. 初始化对象
  3. 将对象的内存地址赋给引用

但是当经过指令重排序后,会变成:

  1. 为对象分配内存
  2. 将对象的内存地址赋给引用(会使得singleton != null)
  3. 初始化对象

所以就存在一种情况,当线程A已经将内存地址赋给引用时,但实例对象并没有完全初始化,同时线程B判断singleton已经不为null,就会导致B线程访问到未初始化的变量从而产生错误。

案例3
 
public class Singleton {
    private static volatile Singleton singleton;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

以上代码对singleton变量添加了volatile修饰,可以阻止局部指令重排序

那么为什么volatile可以保证变量的可见性和阻止指令重排序?

volatile

原理
  1. 规定线程每次修改变量副本后立刻同步到主内存中,用于保证其它线程可以看到自己对变量的修改
  2. 规定线程每次使用变量前,先从主内存中刷新最新的值到工作内存,用于保证能看见其它线程对变量修改的最新值
  3. 为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障防止指令重排序
注意:
  1. volatile只能保证基本类型变量的内存可见性,对于引用类型,无法保证引用所指向的实际对象内部数据的内存可见性。关于引用变量类型详见:Java的数据类型。

  2. volilate只能保证共享对象的可见性,不能保证原子性:假设两个线程同时在做x++,在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量,所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是2。


http://www.kler.cn/a/232985.html

相关文章:

  • 【pytorch】常用强化学习算法实现(持续更新)
  • 01-Ajax入门与axios使用、URL知识
  • FreeRTOS学习13——任务相关API函数
  • scrapy爬取中信证券销售金融产品信息
  • GitLab 如何跨版本升级?
  • Linux——gcc编译过程详解与ACM时间和进度条的制作
  • 系统架构24 - 软件架构设计(3)
  • 已解决org.springframework.aop.AopInvocationException异常的正确解决方法,亲测有效!!!
  • Python中的嵌套字典访问与操作详解
  • VR全景技术可以应用在哪些行业,VR全景技术有哪些优势
  • 无心剑汉英双语诗《龙年大吉》
  • Docker概述
  • 《MySQL 简易速速上手小册》第4章:数据安全性管理(2024 最新版)
  • LabVIEW热电偶自动校准系统
  • FastDFS安装并整合Openresty
  • 【SpringBoot】JWT令牌
  • 【正式】今年第一篇CSDN(纯技术教学)
  • python29-Python的运算符之in运算符
  • Redis实现秒杀
  • SpringCloud-Ribbon实现负载均衡
  • Linux操作系统基础(六):Linux常见命令(一)
  • Python进阶:标准库
  • PySpark(三)RDD持久化、共享变量、Spark内核制度,Spark Shuffle、Spark执行流程
  • QT学习(五)C++函数重载
  • CISCRISC? CPU架构有哪些? x86 ARM?
  • lnmp指令