Java并发编程之内存模型

2022-12-08  本文已影响0人  宏势

一、什么是JMM

JMM即Java内存模型(Java memory model),JSR-133规范中指出JMM是一组规范或者规则,这个规范决定一个线程对共享变量的写入何时对另一个线程可见。简单来说,Java多线程通讯是通过共享内存实现的,但共享内存通讯会存在一系列如可见性、原子性、顺序性等问题,JMM就是围绕着其相关的一系列特性而建立的模型。JMM定义了一些语法集,这些语法集映射Java语言就是volatile、synchronized、final等关键字。


内存模型.png
顺序一致性内存模型

顺序一致性内存模型是一个理想化的理论参考模型,提供了极强的内存可见性。它有两大特性:

JMM和处理器内存模型或者其它内存模型在设计时通常是以顺序一致性内存模型为参照。只是为了提升执行性能,JMM和处理器内存模型会对顺序一致性模型做一些放松,以便做一些处理器和编译器优化。内存模型越松,处理器和编译器可以做更多的优化来提升性能。

指令重排序
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分为3种:

二、实现原理

happens-before 原则

JSR-133通过happens-before 原则来阐述操作之间的内存可见性,如果一个操作执行结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这两个操作可以在同一个线程内,也可以在不同线程内。
规则如下:

as-if-serial 语义

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

as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变,。两个这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

内存屏障: CPU是通过内存屏障指令达到禁止指令重排序

JMM通过语法指令建立happens-before关系,编译器在生成字节码时,会在指令序列中插入内存屏障来防止指令重排序最终生成内存屏障指。常用语法如下:

1.volatile

声明volatile变量被修改时,会将修改后的变量直接写入主存中,并且将其他线程中该变量的缓存置为无效,从而让其它线程对该变量的引用直接从主存中获取数据,这样就保证了变量的可见性。

编译器遇到volatile变量,会在指令序列中插入内存屏障,防止前后指令重排序

volatile修饰的变量i, i++、i+=这类的操作在多线程下都是不能保证变量的原子性的,简单说volatile保证可见性,禁止指令重排序,但不保证原子性

2.锁

锁的释放/获取 与 volatile写/读有相同的语义:

1.线程解锁前必须把共享变量的值刷回主内存
2.线程加锁前必须从主内存读取最新的值到工作内存

3.final

对于final域,编译器和处理器都要遵守两个重要排序规则:

三、案例分析

线程安全的单例模式

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

每次获取实例都需要加锁,且锁synchronized 本身存在较大性能开销,所以引出双重检查锁定的方案,如下:

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 = new Singleton() 语句不是一个原子指令,可以分成三行伪代码:

memory = allocate();  //1:分配对象的内存空间
ctorInstance(memory)  // 2:初始化对象
singleton = memory    // 3:初始化对象

第2行与第3行伪代码可能会被重排序,编程3在2之前,导致代码读取到singleton不为null时,singleton引用的对象有可能还没有完成初始化。比如:当线程A已经将内存地址赋给引用时,但实例对象并没有完全初始化,同时线程B判断singleton已经不为null,就会导致B线程访问到未初始化的变量从而产生错误。

有两种解决方案

1.基于volatile的解决方案

public class Singleton {
    private volatile static Singleton singleton;  //singleton声明为volatile
    private Singleton(){}
    public static Singleton getInstance() {
        if (singleton == null) {                //第一次检查
            synchronized (Singleton.class){     //加锁
                if (singleton == null) {        //第二次检查
                    singleton = new Singleton();//问题的根源
                }
            }
        }
        return singleton;
    }
}

2.基于类初始化解决方案

public class InstanceFactory {
    private static class Singleton{
        public static Singleton singleton = new Singleton();
    }
    public static Singleton getInstance(){  //导致Singleton类被初始化
        return Singleton.singleton;
    }
}

Java语言规范定义,对于每个类或接口C,都有一个唯一的初始化锁LC与之对应,JVM在类初始化期间会获取这个初始化锁,保证初始化只会执行一次,并且每个线程至少获取一次锁来确保这个类已经被初始化过了。

上一篇下一篇

猜你喜欢

热点阅读