Java内存模型
在并发起源中,说到了java安全性问题,是因为多线程的可见性、有序性、原子性三个问题引起,是多线程bug的起源,它们是多线程中的共性问题,java作为多线程编程中处于领先地位的人,自然有针对于此的解决方案,先说说解决可见性和有序性问题的方法,Java内存模型
什么是java内存模型
- 当Java并发程序出现问题时,对代码的检查,消除bug,需要我们对三个bug之源根源了解,还要了解java解决三者问题的方式,才能解决问题。我们知道,导致可见性是因为缓存、导致有序性是因为编译优化,那我们如果直接禁用两者,那么服务性能将十分堪忧;为了解决这个问题,我们就需要按需禁用,分场景和实际需要来禁用;站在我们程序猿的角度,就是让程序按照我们指定的场景禁用缓存和优化。
- Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方式,具体来说,他们包括volatile、synchronized和final三个关键字和Happens-before规则。
Volatile
volatile关键字并不是java特有的,在C语言里就有,它最原始的语意是禁用CPU缓存。
//告诉编译器,对这个变量的读写,不能使用CPU缓存,必须从内存中读取或者写入 volatile int x = 0;
这个语意看上去十分明确,但是在使用时,却会带来困惑。
class VolatileExample{ int x = 0; volatile boolean v = false; public void write(){ x = 42; v = true; } public void read(){ if(v == true){ System.out.println(x); } } }
假如线程A执行write方法,按照volatile语意,会把变量v=true存入内存,假设线程B执行read方法,同样按照volatile语意,线程B会从内存中读取变量v,当v==true时,线程B看到的会是多少了。如果jdk低于1.5,可能是42,也可能是0,高于等于1.5,看到的x一定是42。
Happens-before
望文生义,很多书面也是这么解释的,叫做先行发生,其实他并不是说前面一个操作发生在后续操作的前面,它真正要表达的意思是:前面一个操作的结果对后续操作是可见的,其规则就是要保证线程之间操作的结果可以及时被感知。这种行为,虽然允许编译器优化,但是要求编译器优化后一定要遵守Happens-before规则。
happens-before是java内存模型中最晦涩的内容,和程序猿相关的规则有六项,都是关于可见性的。
1.程序的顺序性规则
指在一个线程中,按照程序顺序,前面的操作happens-before后续的任意操作。比如上面的代码中,第六行x=42;
Happens-before与第七行代码v=true;
,这就是规则1的内容,也比较符合单线程里面的思维,程序前面对某个变量的修改,一定是对后续操作可见的。
2.volatile变量规则
是指对一个volatile变量的写操作,happens-before于后续对这个变量的读操作。就是说禁用缓存,写操作强制刷新到内存,单看此规则与1.5之前没有区别,但是和第三条联合起来,就有区别了。
3.传递性
这条规则是指,A happens-before于B,B happens-before于C,那么A一定happens-before与C。
线程a --> v=42
v=42 --> v=true
线程b --> v==true
v==true --> 读变量x
4.管程中的锁规则
管程中对一个锁的解锁happens-before于后续线程对这个锁的加锁。这个规则中,管程指的是一种通用的同步原语,在Java中指的就是synchronized,是java对管程的实现。管程中的锁在Java中是隐式实现的,例如下面的代码,进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁解锁都是编译器自动帮我们实现。
synchronized (this){
//x是共享变量,初始值10
if(this.x <12){
this.x = 12;
}
}
线程a进入代码块,加锁,对x赋值为12,程序执行结束并释放所,那么后续线程再执行这段代码时,x的值为12对于后续线程是可见的。
5.线程start规则
指主线程a启动子线程b后,子线程b能够看到主线程在启动子线程b之前的操作。如果a线程调用b线程的start方法(在a中启动b),那么该start操作happens-before于线程b的任意操作。
@Test
public void testStart(){
Thread a = new Thread(() -> {
final int var = 66;
System.out.println(var);
Thread b = new Thread(() -> System.out.println("线程b看到的var是:"+var));
// 此处对共享变量 var 修改var = 77;然后再启动线程b,其实b也是可以看到的。
b.start();
});
a.start();
}
6.线程join规则
主线程A中调用线程B并等待线程B操作完成,在线程A中调用线程B的join方法实现即可实现,线程A可以看到线程B的所有操作。线程B的任意操作happens-before于线程B的join操作。
@Test
public void testStart(){
Thread a = new Thread(() -> {
final int var = 66;
Thread b = new Thread(() -> {
//加入这里修改了var,var=77;线程b执行结束后,线程a就可以看见var变成了77
System.out.println("线程b看到的var是:"+var)
});
b.start();
try {
b.join();//等待线程b执行结束
} catch (InterruptedException e) {
e.printStackTrace();
}
//这里线程a其实已经可以看到var变成了77,由于函数式编程导致无法通过修改var来观测,
//但是该条规则实际意思是这个意思
System.out.println("我要等到线程b输出后才能输出,我看到var是77");
});
a.start();
}
7.线程中断规则
对线程interrupt()方法的调用happens-before于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
8.对象终结规则
一个对象的初始化完成(构造函数执行结束)happens-before于它的finalize()方法的开始。