JMM是一种基于共享内存的并发模型,
它是一种抽象概念,规定了一种规范,规定了程序中变量的访问方式,
它规定所有变量都存储在主内存进而进行线程通信,而线程操作变量必须在工作内存进行。
线程,主内存,工作内存三者进行交互
主内存:
主要存储Java实例对象,不管该实例对象是成员变量还是方法的本地变量
或者是类信息,常量,静态变量
工作内存:
存储当前方法的本地变量,是主内存中变量的副本拷贝。
就算两个线程执行同一段代码,都会各自创建自己的工作内存,无法相互访问工作内存
目前计算机一般拥有多个cpu,每个cpu可能存在多个核心。这样就支持多任务并行。
从多线程调度角度来说,每个线程都会映射到各个 cpu核心并行运行。
CPU有一组寄存器,存放内存数据的拷贝。但是cpu处理速度远远高于内存,首先从缓存中读取数据,读取不到再从内存中读取。
对于硬件内存只有寄存器,缓存内存,主内存的概念,并没有工作内存和主内存之分。
java内存模型只是一种抽象概念,不管是工作内存和主内存都存储再计算机内存中。
问题:如果存在多个线程同时对基本类型的变量进行操作,他们的读写是会存在相互干扰的
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排
线程 1 线程 2
1: x2 = a ; 3: x1 = b ;
2: b = 1; 4: a = 2 ;
编译器对这段程序代码执行重排优化后
线程 1 线程 2
2: b = 1; 4: a = 2 ;
1:x2 = a ; 3: x1 = b ;
结论:在多线程环境下,编译器优化重排,会导致结果不一致问题
class MixedOrder{
int a = 0;
boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void read(){
if(flag){
int i = a + 1;
}
}
}
线程A 线程B
writer: read:
1:flag = true; 1:flag = true;
2:a = 1; 2: a = 0 ; //误读
3: i = 1 ;
由于指令重排的原因,线程a的flag置为true提前执行了。
指令重排只会保证单线程串行语义的一致性,但并不会关心多线程间的语义一致性
有序性是指按顺序执行。
编译器优化还是处理器优化重排,都会导致程序执行顺序问题
可见性值得是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
在多线程环境下,共享变量都是拷贝到工作内存进行操作的,然后再写回主内存。
工作内存与主内存同步延迟问题造成了可见性问题。
编译器优化还是处理器优化重排,都会导致程序执行顺序问题,间接导致可见性问题。
仅用sybchronized和volatile关键字保证原子性,可见性,有序性,那么编写并发程序可能显得十分麻烦
作用:在Java内存模型中,还提供了happens-before 原则来帮助程序员判断数据是否存在竞争、线程是否安全
在现代操作系统上编写并发程序时,除了要注意线程安全性问题(多个线程互斥访问临界资源)以外,还要注意多线程对共享变量的可见性
上述8条原则无需手动添加任何同步手段(synchronized|volatile)即可达到效果
下面我们结合前面的案例演示这8条原则如何判断线程是否安全
class MixedOrder{
int a = 0;
boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void read(){
if(flag){
int i = a + 1;
}
}
}
线程A调用实例对象的writer方法,线程B调用实例对象的read方法,线程A先启动,线程B后启动。
存在两线程同时调用,程序次序原则不满足。
两方法都没有使用同步手段,锁规则不满足。
没有使用volatile,volatile变量原则不满则
上述代码没有适合8条原则中的任意一条,也没有使用任何同步手段,所以上述的操作是线程不安全的,因此线程B读取的值自然也是不确定的
解决:要么给writer()方法和read()方法添加同步手段,如synchronized
给变量flag添加volatile关键字,确保线程A修改的值对线程B总是可见
但是对于volatile变量运算操作在多线程环境并不保证安全性
public class VolatileVisibility {
public static volatile int i =0;
public static void increase(){
i++;
}
}
i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成
因此对于increase方法必须使用synchronized修饰,以便保证线程安全,需要注意的是一旦使用synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就完全可以省去volatile修饰变量。
public class VolatileVisibility {
public static int i =0;
public synchronized static void increase(){
i++;
}
}
使用volatile修饰变量达到线程安全目的
public class VolatileSafe {
volatile boolean close;
public void close(){
close=true;
}
public void doWork(){
while (!close){
System.out.println("safe....");
}
}
}
由于对于boolean变量close值的修改属于原子性操作,因此可以通过使用volatile修饰变量close,使用该变量对其他线程立即可见
内在机制:
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,
当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量
内存屏障,又称内存栅栏,是一个CPU指令
作用有两:
JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性