java内存模型:
java虚拟机规范中定义的java内存模型(JMM)是为了 屏蔽各种操作系统 ,硬件等内存的访问差异,以确保Java程序在所有操作系统和平台上能够实现一次编写、到处运行的效果。
理解java内存模型是学习java并发的基础,JMM定义了java中多线程对共享变量操作的定义
主内存
java虚拟机规范规定所有的共享变量存储在主内存中
工作内存
每个线程有自己的独立内存空间--工作内存;
每个线程不能直接操作主内存中的变量,只能从主内存copy到自己的工作内存空间进行操作,然后写入到主内存空间; 各个线程是不可见的数据不共享的, 线程之间的通信需要通过主内存进行交互;所以在多线程进行操作的时候就会出现数据一致性问题
工作内存和主内存交互协议
lock(锁定):作用于主内存变量,锁定变量为线程独占状态
unlock(解锁): 作用于主内存变量,解锁线程锁定的变量,供其他线程使用
read(读取):作用于主内存变量,把一个变量的值从主内存中传输到线程工作内存中,以便随后的load操作
load(载入): 作用于工作内存变量,把从主内存中读取到的值保存到工作内存的变量副本中
use(使用): 作用于工作内存变量,把工作内存中的变量值传递给执行引擎,每当虚拟机需要使用这个变量的时候,就会执行该操作
assign(赋值):作用于工作内存中的变量,把从执行引擎中获取到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的指令的时候会执行该操作
store(存储):作用于工作内存变量,把工作内存中变量的值传递给主内存,以便后续的write操作
write(写入):作用于主内存变量,把store操作过来的值写入到主内存变量中
这8个步骤必须符合的规则
- 不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
- 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存
- 一个新的变量只允许在主内存中诞生,不允许工作内存直接使用未初始化的变量
- 不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何assign操作,是不允许将该变量的值回写到主内存
- 一个变量在同一时刻只能被一个线程进行lock操作,加锁后其他线程不能对该变量进行lock操作,只能等待解锁unlock后其他线程才能操作;但是同一线程对该变量能进行多次lock操作,不过也要相应执行多次的unlock
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
- 不允许对没有lock的变量执行unlock操作,如果一个变量没有被lock操作,那也不能对其执行unlock操作,当然一个线程也不能对被其他线程lock的变量执行unlock操作
- 对一个变量进行unlock操作的时候,必须把变量值同步回主内存
原子性,可见性 和有序性
- 原子性:操作是不可中断的,要么成功要么失败;java内存模型中定义了8种操作都是原子操作,不可再分的;
由Java内存模型来直接保证的原子性变量 操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。如果应用场景需要一个更大方位的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反应到Java代码中就是同步块–synchronized关键字,因此在synchronized块之间的操作也具备原子性。
所以可以这么说JMM中定义的 read,load,assign,use,stoe,write都是原子性操作,lock和unlock是为了在多线程情况下保证原子性 - 可见性:可见性是指当一个线程修改了变量的值,其他线程能够立即感知到;变量值修改后刷新会主内存,使用变量的值时从主内存刷新回工作内存;volatile修饰的变量在操作的时候是具备可见性的,除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final;而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,其他字段就能看见
final字段的值,也就是final域能确保初始化过程的安全性。
- 有序性:单线程中所有的操作都是有序的;如果在多线程环境下,一个线程观察其他线程的操作都是无序的;前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象; 也可以这么说 单线程中不存在有序和无序,只有在多线程环境下多个线程操作顺序是乱序的
这里引入一个指令重排序的概念; 编译器在不改变程序的语义的情况下对代码指令进行重排序;
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。例如:
int a = 10;
int b = 20;
这样的操作 可能会先加载 int b = 20; 两个变量的加载顺序是不关联的
int a = 10;
int b = a;
这样的操作是和顺序由关联的 必须先加载a, 在加载 b
再看下面代码
// 定义两个变量
int a = 0;
boolean flag = false;
public void opration1() {
a = 2; // 1
flag = true; // 2
}
public void opration2() {
if (flag) { // 3
System.out.print(a * a);//4
}
}
现在假设有两个线程,一个线程调用了opration1 方法, 一个线程调用了opration2 方法, 由于指令重排序 线程1 可能是这么执行的
flag = true; // 先执行了flag = true 并写入了主内存
a = 2;
然后这个时候 线程而开始执行了,线程2 获取到的flag = true, a = 0;(线程1 的a赋值操作还没写入到主内存) 所以会输出 0 ;
再看一个经典的单例问题, 双重检测机制
//常见的 线程安全的单例模式
public class TestA {
private TestA(){}
public static TestA ta = null;
public static TestA getInstance() {
if (ta == null) {
synchronized (TestA.class) {
if (ta == null) {
ta = new TestA();
}
}
}
return ta;
}
}
但是他一定安全吗/
ta = new TestA();
这里会做三步操作;
- 开辟内存空间,分配内存空间
- 初始化对象信息
- 句柄引用指向内存空间
在指令重排序的作用下 原本 1-2-3 的操作 可能会变成1-3-2 也就是分配空间后并没有初始化就进行了 对象的引用指向操作,
所以这个 单例模式 在极小的概率下并不是安全的