深入理解Java内存模型详解

artech 阅读:12 2022-11-11 15:40:25 评论:0

原子性
原子性即一个操作或一系列是不可中断的。即使是在多个线程的情况下,操作一旦开始,就不会被其他线程干扰。

比如,对于一个静态变量int x两条线程同时对其赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的。

Java内存模型对以下操作保证其原子性:read,load,assign,use,store,write。我们可以大致认为基本数据类型的访问读写是具备原子性的(前面也提到了long和double类型的“半个变量”情况,不过几乎不会发生)。

从Java内存模型底层来看有上面的原子性操作,但针对用户来说,也就是我们编写Java的程序,如果需要更大范围的原子性保障,就需要同步关键字——synchronized来保障了。也就是说synchronized中的操作也具有原子性。

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

Java内存模型是通过变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值,将主内存作为传递媒介。可回顾一下上篇文章的图。

无论普通变量还是volatile变量都是如此,只不过volatile变量保证新值能够立马同步到主内存,使用时也立即从主内存刷新,保证了多线程操作时变量的可见性。而普通变量不能够保证。

除了volatile,synchronized和final也能够实现可见性。

synchronized实现的可见性是通过“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”来保证的。

主要有两个原则:线程解锁前,必须把共享变量的最新值刷新到主内存中;线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

final的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那在其他线程中就能看见final的值。

有序性
在Java内存模型中有序性可归纳为这样一句话:如果在本线程内观察,所有操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的。

有序性是指对于单线程的执行代码,执行是按顺序依次进行的。但在多线程环境中,则可能出现乱序现象,因为在编译过程会出现“指令重排”,重排后的指令与原指令的顺序未必一致。

因此,上面归纳的前半句指的是线程内保证串行语义执行,后半句则指指“令重排现”象和“工作内存与主内存同步延迟”现象。

同样,Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。

指令重排
计算机执行指令经过编译之后形成指令序列。一般情况,指令序列是会输出确定的结果,且每一次的执行都有确定的结果。

CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化。但代码逻辑之间是存在一定的先后顺序,并发执行时按照不同的执行逻辑会得到不同的结果。

编译器优化重排序:编译器在不改变单线程程序语义的前提下,重新安排语句执行顺序。

指令级并行重排序:处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应及其的执行顺序。

内存系统的重排序:处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去可能是乱序执行。

举个例来说明一下多线程中可能出现的重排现象:

class ReOrderDemo {    int a = 0;    boolean flag = false;    public void write() {        a = 1;                   //1        flag = true;             //2    }        public void read() {        if (flag) {                //3            int i =  a * a;        //4            ……        }    } }

在上面的代码中,单线程执行时,read方法能够获得flag的值进行判断,获得预期结果。但在多线程的情况下就可能出现不同的结果。

比如,当线程A进行write操作时,由于指令重排,write方法中的代码执行顺序可能会变成下面这样:

flag = true;             //2a = 1;                   //1

也就是说可能会先对flag赋值,然后再对a赋值。这在单线程中并不影响最终输出的结果。

但如果与此同时,B线程在调用read方法,那么就有可能出现flag为true但a还是0,这时进入第4步操作的结果就为0,而不是预期的1了。

请记住,指令重排只会保证单线程中串行语义执行的一致性,不会关心多线程间语义的一致性。这也是为什么在写单例模式时需要考虑添加volatile关键词来修饰,就是为了防止指令重排导致的问题。

JMM提供的解决方案
在了解了原子性、可见性以及有序性问题后,看看JMM是提供了什么机制来保证这些特性的。

原子性问题,除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性。

而工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized关键字或者volatile关键字解决。它们都可以使一个线程修改后的变量立即对其他线程可见。

对于指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决。volatile的另一个作用就是禁止重排序优化。

除了靠sychronized和volatile关键字之外,JMM内部还定义一套happens-before(先行发生)原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。


本文参考链接:https://www.yisu.com/zixun/91813.html
标签:java
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

我的关注

全民解析

搜索
关注我们