前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java多线程内存模型(JMM)

Java多线程内存模型(JMM)

作者头像
chenchenchen
发布2022-01-05 14:12:45
3520
发布2022-01-05 14:12:45
举报
文章被收录于专栏:chenchenchen

CUP多核并发缓存架构

CPU缓存一致性协议MESI

CPU高速缓存(Cache Memory)

CPU在摩尔定律的指导下以每18个月起一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。

CPU在摩尔定律的指导下以每18个月起一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。

为了解决这个问题,CPU厂商在CPU中内置了少量的高速毁存以解决T\O速度和CPU运算速度之间的不P有问题。

在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理

  • 时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。比加循环、递归、方法的反复调用等。
  • 空间局部性(Spatlal Locality):如果一个存储器的位置被引用。那么将来他附近的位置也会被引用。比如顺序执行的代码、连续创建的两个对象、数组等。

带有高速缓存的CPU执行计算的流程

  • 程序以及数据被加载到主内存
  • 指令和数据被加载到CPU的高速缓存
  • CPU执行指令,把结果写到高速缓存
  • 高速缓存中的数据写回主内存

多核CPU多级缓存一致性协议MESI

多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。

MESI是指4中状态的首字母。每个Cache line有4个状态,可用2个bit表示。

数据的可见性,有序性和原子性

原子性

什么是原子性

原子性指一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分的。

原子性怎么实现

  • 使用synchronized或Lock加锁实现,保证任一时刻只有一个线程访问该代码块
  • 使用原子操作

Java中的原子操作有哪些

  • 除long和double之外的基本类型的赋值操作(64位值,当成两次32位的进行操作)
  • 所有引用reference的赋值操作
  • java.concurrent.Atomic.*包中所有类的原子操作

创建对象的过程是否是原子操作

创建对象实际上有3个步骤,并不是原子性的(常应用于双重检查+volatile创建单例场景)

  • 创建一个空对象
  • 调用构造方法
  • 创建好的实例赋值给引用

可见性

什么是可见性问题

可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。

为什么会有可见性问题

对于多线程程序而言,线程将共享变量拷贝到各自的工作内存进行操作。线程A读取共享变量后,其他线程再对共享变量的修改,对于线程A来说并不可见,这就造成了可见性问题。

如何解决可见性问题

  • 加volatile关键字保证可见性。共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到。
  • 使用synchronized和Lock保证可见性。保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。

volatile,synchronized可见性,有序性,原子性代码证明:https://blog.csdn.net/duyabc/article/details/111561857

有序性(重排序)

什么是重排序

在线程内部的两行代码的实际执行顺序和代码在Java文件中的逻辑顺序不一致,代码指令并不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序。

重排序的意义

JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

重排序的3种情况

  • 编译器优化( JVM,JIT编辑器等): 编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序
  • 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序: 由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

重排序遵循原则

重排序会遵循as-if-serial与happens-before原则

as-if-serial原则

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

为了遵守as-f-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

happens-before原则

从JDK5开始,提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before原则内容如下:

  • 1、程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  • 2、锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加晚的动作必须在解锁动作之后(同一个锁)。
  • 3、volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatle变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  • 4、线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见。
  • 5、传递性:A先于B,B先于C那么A必然先于C。
  • 6、线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  • 7、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  • 8、对象终结规则:对象的构造函数执行,结束先于finalize()方法。

双重检测锁DCL(DoubleCheckLock)对象半初始化问题

双重检查锁定DCL减少了锁粒度,不需要对整个getInstance()方法加synchronized锁,提高了方法被多个线程频繁的调用时的性能。

在对象创建好之后,执行getInstance()方法将不需要获取锁,检查instance不为空,就直接返回。

代码语言:javascript
复制
public class Singleton {
    // volatile禁止指令重排,保证构造方法中的数据实例化
    //private static volatile Singleton 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没有加volatile,代码instance = new Singleton();创建一个对象时,这一行代码可以分解为如下的三行伪代码:

代码语言:javascript
复制
// instance = new Singleton();
memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory);  //2:初始化对象
instance = memory;     //3:设置instance指向刚分配的内存地址

而代码中的2和3之间,可能会被重排序。即instance先指向刚分配的内存地址,之后再进行对象的初始化。这可能会导致一个线程可能读到尚未初始化的Bean,而这个instance的确是!=null的。

测试代码:

代码语言:javascript
复制
@Data
public class Singleton {

    private static Singleton instance;
    private String name;

    public static Singleton getInstance() throws InterruptedException {
        System.out.println("运行 "+Thread.currentThread().getName());
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                    System.out.println("new Singleton()执行完毕");
                    //耗时的初始化
                    TimeUnit.SECONDS.sleep(3);
                    instance.setName("chenchenchen");
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Singleton instance = Singleton.getInstance();
                    System.out.println("get name :"+instance.getName().toString());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(runnable);
        thread1.start();
        thread2.start();
        Thread.sleep(2000);
        thread3.start();
    }
}

运行结果:

代码语言:javascript
复制
运行 Thread-0
运行 Thread-1
new Singleton()执行完毕
Exception in thread "Thread-0" java.lang.NullPointerException
	at com.aspire.mall.task.carwash.job.Singleton$1.run(Singleton.java:39)
	at java.lang.Thread.run(Thread.java:748)
运行 Thread-2
Exception in thread "Thread-2" java.lang.NullPointerException
	at com.aspire.mall.task.carwash.job.Singleton$1.run(Singleton.java:39)
	at java.lang.Thread.run(Thread.java:748)
get name :chenchenchen

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tOqQa063-1623570849031)(upload%5Cimage-20210605205532011.png)]

我们可以想出两个办法来实现线程安全的延迟初始化。

  • 不允许2和3重排序(在JDK 1.5后可以基于volatile来解决);
  • 允许2和3重排序,但不允许其他线程“看到”这个重排序(可以使用静态内部类解决);

基于volatile的双重检查锁定的解决方案

代码语言:javascript
复制
 private volatile static Instance instance;

基于类初始化的解决方案

代码语言:javascript
复制
public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }

    public static Instance getInstance() {
    	//InstanceHolder类在这里会被初始化
        return InstanceHolder.instance ;  
    }
}

JMM多线程内存模型

通俗来说,JMM是一套多线程读写共享数据时,对数据的可见性,有序性和原子性的规则。

JVM实现不同会造成“翻译”的效果不同,不同CPU平台的机器指令有千差万别,无法保证同一份代码并发下的效果一致。所以需要一套统一的规范来约束JVM的翻译过程,保证并发效果一致性。

Java多线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的。Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

JMM数据原子操作

  • read(读取)︰从主内存读取数据
  • load(载入)︰将主内存读取到的数据写入工作内存.
  • use(使用)∶从工作内存读取数据来计算
  • assign(赋值)∶将计算好的值重新赋值到工作内存中.
  • store(存储)∶将工作内存数据写入主内存
  • write(写入):将store过去的变量值赋值给主内存中的变量.
  • lock(锁定)∶将主内存变量加锁,标识为线程独占状
  • unlock(解锁)︰将主内存变量解锁,解锁后其他线程可以锁定该变量

happens-before规则

即前一个操作的结果可以被后续的操作获取。

  • 程序的顺序性规则:在一个线程内一段代码的执行结果是有序的。虽然还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
  • volatile规则: 就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
  • 传递性规则:happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
  • 管程锁定规则:无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
  • 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
  • 线程终止规则: 在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
  • 线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
  • 对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

volatile关键字

什么是volatile

volatile是一种同步机制,比synchronized或者Lock相关类更轻量级,因为使用volacile并不会发生上下文切换等开销很大的行为 volatile是无锁的,并且只能修饰单个属性

什么时候适合用vilatile

一个共享变量始终只被各个线程赋值,没有其他操作 作为刷新的触发器,引用刷新之后使修改内容对其他线程可见(如CopyOnRightArrayList底层动态数组通过volatile修饰,保证修改完成后通过引用变化触发volatile刷新,使其他线程可见)

volatile的作用

可见性保障:修改一个volatile修饰变量之后,会立即将修改同步到主内存,使用一个volatile修饰的变量之前,会立即从主内存中刷新数据。保证读取的数据都是最新的,之前的修改都是可见的。

有序性保障(禁止指令重排序优化):有volatile修饰的变量,赋值后多了一个“内存屏障“( 指令重排序时不能把后面的指令重排序到内存屏障之前的位置)

volatile的性能

volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

volatile缓存可见性实现原理

lock前缀指令 + MESI缓存一致性协议

对volatile修饰的变量执行写操作时,JVM会发送一条lock前缀指令给CPU,CPU在执行写操作之后会立即将这个值写回主内存。

同时因为有MESI缓存一致性协议,各个CPU都会对总线进行嗅探,如果本地缓存中的数据被修改了,就会将自己本地缓存的数据过期掉。再次读取变量时,就会从主内存重新加载最新的数据了。

如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了

IA-32和Intel 64架构软件开发者手册对lock指令的解释:

  • 会将当前处理器缓存行的数据立即写回到系统内存。
  • 在其他CPU里缓存了该内存地址的数据变为无效(MESI协议)。
  • 提供内存屏障功能,使lock前后指令不能重排序。

volatile有序性实现原理

内存屏障

  • 每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和volatile写重排;
  • 每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排;
  • 每个volatile读操作后面,加LoadLoad屏障,禁止跟下面的普通读和voaltile读重排;
  • 每个volatile读操作后面,加LoadStore屏障,禁止跟下面的普通写和volatile读重排;

LoadLoad读读屏障:确保Load1数据的装载先于Load2后所有装载指令,Load1对应的代码和Load2对应的代码,是不能指令重排的。

代码语言:javascript
复制
Load1:int localVar = this.variable
LoadLoad读读屏障
Load2:int localVar = this.variable2

StoreStore写写屏障:确保Store1的数据一定刷回主存,对其他cpu可见,先于Store2以及后续指令。

代码语言:javascript
复制
Store1:this.variable = 1
StoreStore写写屏障
Store2:this.variable2 = 2

LoadStore读写屏障:确保Load1指令的数据装载,先于Store2以及后续指令。

代码语言:javascript
复制
Load1:int localVar = this.variable
LoadStore读写屏障
Store2:this.variable = 2

StoreLoad写读屏障:确保Store1指令的数据一定刷回主存,对其他cpu可见,先于Load2以及后续指令的数据装载

代码语言:javascript
复制
Store1:this.variable = 2
StoreLoad写读屏障
Load2:int localVar = this.variable

Java程序汇编代码查看

下载查看运行代码的汇编指令,放到JDK所在的/jre/bin路径下。

测试代码

JVM启动参数添加上VolatileVisibilityTest.java的测试类名的执行方法prepareData:-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileVisibilityTest.prepareData

参考:

JVM内存结构和Java内存模型别再傻傻分不清了:https://blog.csdn.net/qq_41170102/article/details/104650162

双重检查锁定(DCL)与延迟初始化:https://blog.csdn.net/u013190088/article/details/83154443

双重检查锁(DCL)问题:https://blog.csdn.net/Dongguabai/article/details/82828125

https://processon.com/view/6061d2ee1e0853028ab68bd5?fromnew=1

https://processon.com/view/5fcb5f777d9c0837c09e0025?fromnew=1

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/06/13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CUP多核并发缓存架构
    • CPU高速缓存(Cache Memory)
      • 多核CPU多级缓存一致性协议MESI
      • 数据的可见性,有序性和原子性
        • 原子性
          • 什么是原子性
          • 原子性怎么实现
          • Java中的原子操作有哪些
          • 创建对象的过程是否是原子操作
        • 可见性
          • 什么是可见性问题
          • 为什么会有可见性问题
          • 如何解决可见性问题
        • 有序性(重排序)
          • 什么是重排序
          • 重排序的意义
          • 重排序的3种情况
          • 重排序遵循原则
          • 双重检测锁DCL(DoubleCheckLock)对象半初始化问题
      • JMM多线程内存模型
        • JMM数据原子操作
          • happens-before规则
          • volatile关键字
            • 什么是volatile
              • 什么时候适合用vilatile
                • volatile的作用
                  • volatile的性能
                    • volatile缓存可见性实现原理
                      • volatile有序性实现原理
                        • Java程序汇编代码查看
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档