3、java内存模型特点

 2019-12-10 15:41  阅读(755)
文章分类:Java Core

java内存模型是围绕着在并发过程中如何处理原子性,可见性跟有序性这三个问题来建立的。先看一下这三个特性:
1、原子性 由java内存模型来直接保证的原子性变量操作就是上文2中提到的8种基本操作,我们大致可以认为基本数据类型的读写是具备原子性的(long跟double不必太过在意)。如果需要一个更大范围的原子性保证,java内存模型还提供了lock跟unlock操作来满足这种需求。 原子性操作值相应的操作是单一不可分割的。例如对int变量count执行count++就不是原子操作。可以分解为3个操作:(1)读取count当前值;(2)count当前值跟1做加法运算;(3)将加完后的值赋给count变量。 多线程环境中,非原子操作可能会受其他线程干扰,比如上述例子,在执行第二个操作的时候,count的值可能已经被其它线程修改了。当然我们可以加锁或者synchronize来避免这种操作。synchronize可以实现原子性,实质是:
通过该关键字所包括的临界区(Critical Section)的排他性保证在任何一个时刻只有一个线程能够执行临界区中的代码,这使得临界区中的代码代表了一个原子操作。这一点,大家基本都很清楚。但是,synchronized关键字所起到的另一个作用——保证内存的可见性(Memory Visibility),也是我们值得回顾的地方。
临界区:每个进程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。多个进程中涉及到同一个临界资源的临界区称为相关临界区
2、可见性 可见性指一个线程修改了共享变量的值,其它线程能够立即得知这个修改。比如volatile变量的修改,
volatile关键字实现内存可见性的核心机制是:当一个线程修改了一个volatile修饰的变量的值时,该值会被写入主内存(即RAM)而不仅仅是当前线程所在的CPU的缓存区,而其他CPU的缓存区中存储的该变量的值也会因此而失效(从而得以更新为主内存中该变量的“新值”)。这就保证了其他线程访问该volatile修饰的变量时,总是可以获取到该变量的最新值。 除了volatile之外,java还有两个关键字能实现可见性:synchronized跟final。synchronize不做赘述,final的特殊规定如下:
与前面介绍的锁和volatile相比较,对final变量的读和写更像是普通的变量访问。对于final变量,编译器和处理器要遵守两个(分别对应读写)重排序规则:
1、在构造函数内对一个final变量的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
2、初次读一个包含final变量的对象的引用,与随后初次读这个final变量,这两个操作之间不能重排序。
写final变量的重排序规则
写final变量的重排序规则禁止把final变量的写重排序到构造函数之外。这个规则的实现包含下面2个方面:
1、JMM禁止编译器把final变量的写重排序到构造函数之外。
2、编译器会在final变量的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final变量的写重排序到构造函数之外 。
写final变量的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final变量已经被正确初始化过了,而普通变量不具有这个保障
对于引用类型,写final变量的重排序规则对编译器和处理器增加了如下约束:
在构造函数内对一个final引用的对象的成员变量的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
读final变量的重排序规则
在一个线程中,初次读对象引用与初次读该对象包含的final变量,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。初次读对象引用与初次读该对象包含的final变量,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。
读final变量的重排序规则可以确保:在读一个对象的final变量之前,一定会先读包含这个final变量的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final变量一定已经被A线程初始化过了。
3、有序性
叫有序性其实有些容易误解,这个实际说的是重排序。线程内表现为串行,线程外表现为无序。就是说虽然执行过程中有重排序的优化问题,但从结果来看,单个线程执行结果并没有改变,但如果没有对公共资源加锁,多个线程并发执行的时候,会发现顺序是乱的,可能跟预期结果不同。
先行发生原则: 先行发生是指如果操作A先行发生于B,则在B操作之前,操作A产生的影响能被B观察到,影响包括修改了内存中共享变量的值,发生了消息调用了方法等。 例如:

//以下操作在线程A中执行
    int i = 1;
    //以下操作在线程B中执行
    j = i;
    //以下操作在线程C中执行
    i = 2

  如果A先行发生于B,但B跟C没有先行发生关系,则B中j的值是不确定的。
  java中存在一些先行发生关系的操作,这些操作不需要同步就可以保证先行发生关系:
  1、程序次序规则。在一个线程内,书写在前面的代码先行发生于后面的。确切地说应该是,按照程序的控制流顺序,因为存在一些分支结构。
**  2、Volatile变量规则。对一个volatile修饰的变量,对他的写操作先行发生于读操作。**
**  3、线程启动规则。Thread对象的start()方法先行发生于此线程的每一个动作。**
**  4、线程终止规则。线程的所有操作都先行发生于对此线程的终止检测。**
**  5、线程中断规则。对线程interrupt()方法的调用先行发生于被中断线程的代码所检测到的中断事件。**
**  6、对象终止规则。一个对象的初始化完成(构造函数之行结束)先行发生于发的finilize()方法的开始。**
**  7、传递性。A先行发生B,B先行发生C,那么,A先行发生C。**
**  8、管程锁定规则。一个unlock操作先行发生于后面对同一个锁的lock操作。**

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 3、java内存模型特点

相关推荐