Java内存模型
1、Java内存模型
- JMM即Java Memory Model,它定义了主存(即JVM中的方法区和堆区)、工作内存(即JVM中的虚拟机栈)抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
- JMM体现在以下几个方面:
- 原子性-保证指令不会受到线程上下文切换的影响。
- 可见性-保证指令不会受cpu缓存的影响。
- 有序性-保证指令不会受cpu指令并行优化的影响。
- JMM体现在以下几个方面:
2、可见性
先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestVisibility {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
}
});
t.start();
Thread.sleep(1000);
log.debug("停止t");
run = false; // 线程t不会如预想的停下来
}
}分析:
①初始状态,t线程刚开始从主内存读取了run的值到工作内存。
②因为t线程要频繁从主内存中读取run的值,JIT 编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率。
③1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。
解决方法:
volatile(易变关键字):它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。volatile保证内存可见性和禁止指令重排,用于多线程环境下的单次操作(单次读或者单次写)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestVisibility {
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
}
});
t.start();
Thread.sleep(1000);
log.debug("停止t");
run = false; // 线程t不会如预想的停下来
}
}使用synchronized()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestVisibility {
static boolean run = true;
final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
synchronized (obj) {
if (!run) {
break;
}
}
}
});
t.start();
Thread.sleep(1000);
log.debug("停止t");
run = false;
}
}
对比可见性和原子性。
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况,上例从字节码理解是这样的:
1
2
3
4
5
6getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false有线程安全问题的例子:两个线程一个 i++ 一个 i– ,只能保证看到最新值,不能解决指令交错:
1
2
3
4
5
6
7
8
9// 假设i的初始值为0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1结论
- synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是
synchronized是属于重量级操作,性能相对更低。
- synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是
happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的可见。
①解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见。
1
2
3
4
5
6
7
8
9
10
11
12
13static int x;
static Object m = new Object();
// 假设t1线程先加锁运行
new Thread(() -> {
synchronized (m) {
x = 10;
}
}, "t1").start();
new Thread(() -> {
synchronized (m) {
System.out.println(x);
}
}, "t2").start();
②线程对volatile变量的写,对接下来其它线程对该变量的读可见。
1
2
3
4
5
6
7
8volatile static int x;
// 假设t1线程先运行
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();③线程start前对变量的写,对该线程开始后对该变量的读可见。
1
2
3
4
5static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();④线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用t1.isAlive()或t1.join()等待它结束)。
1
2
3
4
5
6
7static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
⑤线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过
t2.interrupted 或 t2.isInterrupted)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
sleep(1);
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}⑥对变量默认值(0,false,null)的写,对其它线程对该变量的读可见。
⑦具有传递性,配合volatile的防指令重排,有下面的例子:
1
2
3
4
5
6
7
8
9
10
11volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;
// 写屏障
},"t1").start();
new Thread(()->{
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
},"t2").start();
3、有序性
JVM会在不影响正确性的前提下,可以调整语句的执行顺序,如下面一段代码。
1
2
3
4
5static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;可以看到,至于是先执行i还是先执行j,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是:
1
2i = ...;
j = ...;也可以是:
1
2j = ...;
i = ...;
这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。指令重排的前提是,重排指令不能影响结果,禁止指令重排可以使用volatile关键字实现。
4、volatile原理
- volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对volatile变量的写指令后会加入写屏障。
- 对volatile变量的读指令前会加入读屏障。
4.1 保证可见性
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中。
1 | public void actor2(I_Result r) { |
而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据。
1
2
3
4
5
6
7
8
9public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}内存间的交互操作如下:
此时如果不加volatile关键字:
此时如果加了volatile关键字:
不加volatile的变量只会执行read、load、use、assign操作,即只会在当前线程的工作内存中改变值,而其它线程和主内存无法感知到(虚拟机栈是线程私有的),即不可见;而加了volatile的变量会进行lock、read、load、use、assign、store、write、unlock的操作将变量同步到主存中(lock指令能将缓存的数据写到主内存,并会使在其他线程里缓存了该内存地址的数据无效,即实现了缓存一致性协议;另外,lock指令还会锁总线,锁总线时,其它CPU对内存的读写请求都会被阻塞)
4.2 保证有序性
有序性定义: 指的是在代码顺序结构中,我们可以直观的指定代码的执行顺序, 即从上到下按序执行。但编译器和CPU处理器会根据自己的决策,对代码的执行顺序进行重新排序。优化指令的执行顺序,提升程序的性能和执行速度,使语句执行顺序发生改变,出现重排序,但最终结果看起来没什么变化(单核)。
有序性问题指的是在多线程(多核)由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程,导致的结果与预期不符的问题。这就是编译器的编译优化给并发编程带来的程序有序性问题。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响单线程执行的结果,会影响到多线程并发执行结果的正确性。
通过
volatile
,synchronized
,Lock
保证一定的有序性,synchronized
,Lock
保证每一时刻只有一个线程可以执行同步代码块,相当于让线程顺序执行同步代码,从而保证有序性。另外,JVM具备一些先天的有序性,即不需要额外的手段就能保证有序性,即Happens-before原则,如果两个操作的执行次序没有办法通过Happens-before原则推导出来,虚拟机进行随意的重排序,那么就不能保证有序性。写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。
1
2
3
4
5public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。
1
2
3
4
5
6
7
8
9public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}注意:
- 不能解决指令交错。
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去。
- 而有序性的保证也只是保证了本线程内相关代码不被重排序。
5、double-checked locking问题
以著名的double-checked locking单例模式为例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE == null) { // t2
// 首次访问会同步,而之后的使用没有synchronized
// synchronized可以解决可见性、原子性问题,而解决有序性问题必须要建立在变量完全在synchronized同步代码块里的情况,这里的synchronized外层的if语句判断条件处使用了INSTANCE变量,所以这里的synchronized并不能完全解决指令重排问题。
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}以上的实现特点是:
懒惰实例化。
首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁。
有隐含的,但很关键的一点:第一个if使用了INSTANCE变量,是在同步块之外,但在多线程环境下,上面的代码是有问题的,getInstance方法对应的字节码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
220 getstatic #2 <org/example/Singleton.INSTANCE>
3 ifnonnull 37 (+34)
6 ldc #3 <org/example/Singleton>
8 dup
9 astore_0
10 monitorenter
11 getstatic #2 <org/example/Singleton.INSTANCE>
14 ifnonnull 27 (+13)
17 new #3 <org/example/Singleton> // 创建对象,将对象引用入栈
20 dup // 复制一份对象引用入栈
21 invokespecial #4 <org/example/Singleton.<init>> // 将其中一个对象引用出栈并调用构造方法
24 putstatic #2 <org/example/Singleton.INSTANCE> // 将另一个对象引用出栈并给Singleton.INSTANCE赋值
27 aload_0
28 monitorexit
29 goto 37 (+8)
32 astore_1
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #2 <org/example/Singleton.INSTANCE>
40 areturn重点看上方的第17、20、21和24条指令,也许jvm会优化为:先执行24,再执行21。如果两个线程t1,t2按如下时间序列执行:
- 关键在于0:getstatic这行代码在monitor控制之外,它就像之前举例中不守规则的人,可以越过monitor读取INSTANCE变量的值。
- 这时t1还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2拿到的是将是一个未初始化完毕的单例。
- 对INSTANCE使用volatile修饰即可,可以禁用指令重排,但要注意在JDK 5以上的版本的volatile才会真正有效。
解决后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}getInstance()的字节码指令为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// -------------------------------------> 加入对 INSTANCE 变量的读屏障
0 getstatic #2 <org/example/Singleton.INSTANCE>
3 ifnonnull 37 (+34)
6 ldc #3 <org/example/Singleton>
8 dup
9 astore_0
10 monitorenter -----------------------> 保证原子性、可见性
11 getstatic #2 <org/example/Singleton.INSTANCE>
14 ifnonnull 27 (+13)
17 new #3 <org/example/Singleton>
20 dup
21 invokespecial #4 <org/example/Singleton.<init>>
24 putstatic #2 <org/example/Singleton.INSTANCE>
// -------------------------------------> 加入对 INSTANCE 变量的写屏障
27 aload_0
28 monitorexit ------------------------> 保证原子性、可见性
29 goto 37 (+8)
32 astore_1
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #2 <org/example/Singleton.INSTANCE>
40 areturn如上面的注释内容所示,读写volatile变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:
- 可见性:
- 写屏障(sfence)保证在该屏障之前的t1对共享变量的改动,都同步到主存当中。
- 而读屏障(lfence)保证在该屏障之后t2对共享变量的读取,加载的是主存中最新数据。
- 有序性:
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。
- 更底层是读写变量时使用lock指令来多核CPU之间的可见性与有序性。
- 可见性: