- "Java内存模型"来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
- 主要目的:定义程序中各种变量的访问规则,即关注在虚拟机中
把变量值存储到内存
和从内存中取出变量值
这样的底层细节。 - 这里的变量包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数(它们是线程私有的)
1 主内存与工作内存
- Java内存模型规定了
所有的变量都存储
在 主内存中 - 每条线程还有自己的工作内存,工作内存中
保存了被该线程使用的变量的主内存副本
。- 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。
- 不同的线程之间还无法访问对方工作内存中的变量
- 线程间变量值的传递均需要通过主内存来完成。
2 内存间交互操作
- 主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存这一类的实现细节。
- Java内存模型定义了以下8种操作符来实现。且必须保证下面提及的每一种操作都是原子的、不可再分的。
- 八种操作规则
3 volatile型变量的特殊规则
- 关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制
- 当一个变量被定义成volatile之后,它将具备两种特性:
- 第一项是保证此变量对所有线程的可见性(可见性指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立刻的得知的)
- 但是不代表volatile变量对所有线程是立刻可见的,对volatile变量的所有写操作都能立刻反映到其他线程之中。
- Java里面的运算操作符不是原子操作,这可能会导致volatile变量的运算在并发下一样是不安全的。
public class VolatileTest {
public static volatile int race = 0 ;
public static void increase(){
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for(int i = 0 ; i < THREADS_COUNT ; i++){
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0 ; i < 10000 ; i++){
increase();
System.out.println(race);
}
}
});
//开启当前子线程
threads[i].start();
}
//等待所有累加线程都结束
while (Thread.activeCount() > 1){
Thread.yield();
}
System.out.println(race);
}
}
- 禁止指令重排序优化,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序 与 程序代码中的执行顺序一致。
- 正常情况下,执行顺序是1 2 3 4
- 但是在多线程环境下,执行顺序可能是 2 1 3 4 或者 1 3 2 4 。 这就是指令的重排,也就是内部执行顺序和代码顺序不同。
- 但是指令重排还是有限制的,不会出现下面的顺序, 4 3 2 1。因为要考虑到指令之间的数据依赖性。
//这是指令重排序的案例
public void mySort(){
int x = 11;
int y = 12;
x = x + 5;
y = x * x;
}
Volatile针对指令重排做了啥?
- 禁止了指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象
- 首先需要了解内存屏障的概念,它是一个CPU指令。它的作用有两个
- 保证特定操作的顺序
- 保证某些变量的内存可见性
- 由于编译器和处理器都能执行指令重排的优化,所以如果在指令间插入内存屏障会告诉编译器和CPU,什么指令都不能和这条内存屏障重排序。 通过插入内存屏障进制内存屏障前后的指令执行重排序优化。
4 针对long和double型变量的特殊规则
- Java内存模型要求lock、unlock、read、load、assign、use、store和write这八种操作具有原子性,但是对于64位的数据类型long和double来说,定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据类型的读写操作划分为两次32位的操作来进行。
- **即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性。**这就是long和double1的非原子性协定。
如果有多个线程共享一个为声明为volatile的long或double类型变量,并且同时对他们进行读取和修改操作,那么某些线程可能得到一个既不是原值,也不是修改后的值。
5 原子性、可见性与有序性
原子性
- 根据原子性变量操作(read…)可以大致认为:基本数据类型的访问、读写都是具有原子性的
可见性
- 可见性就是当一个线程修改了共享变量的值时,其他线程能够立刻得知这个修改。
- Java内存,模型是通过在变量修改后将新值同步回主内存、在变量读取前从主内存中刷新变量值来实现的可见性。
- volatile相比普通变量的区别在于 它保证了新值可以立刻同步到主内存。
有序性
- 如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。Java语言提供了volatile和synchronized来保证线程之间操作的有序性。
6 先行发生原则
- Java中有一个"先行发生"的原则,它可以判断数据是否存在竞争、线程是否安全的非常有用的手段。Java内存模型中存在一些"天然的"先行发生关系,可以在编码中直接使用。
- 如果两个操作之间的关系不在此列,并且无法从下列规则中推导出来,说明它们没有顺序性的保障,虚拟机可以随意对它们进行重排序。
- 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。(不是程序代码顺序,需要考虑分支、循环等结构)。
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。
- 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。
- 传递性:如果操作A先行发生于操作B,操作B先行发生于操作A,那么可以得出A先行C