并发系列之JVM内存模型、线程安全可见性

并发系列之JVM内存模型、线程安全可见性

为什么会有内存模型?

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.wyj.jvm.thread;

import java.util.concurrent.TimeUnit;

public class VisibilityDemo {
private boolean flag = true;

// JIT just in time(即时编译器)
public static void main(String[] args) throws InterruptedException{
VisibilityDemo demo1 = new VisibilityDemo();
System.out.println("代码开始了");
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (demo1.flag){
i++;
}
System.out.println(i);
}
});
thread1.start();
TimeUnit.SECONDS.sleep(2);
// 设置is为false;使上面的线程结束while循环
demo1.flag = false;
System.out.println("被置为false了");
}
}

执行结果

1
2
代码开始了
被置为false了

一直卡在while循环里面,没有i值打印出来

为什么不停止???

如何让他正确停止

1:volatile

1)根据内存模型的规定,保持可见性

2)ACC_VOLATILE访问控制,可以保证没有缓存立马可见

2:启动参数-client

-client(或 -server -Djava.compiler=NONE ——— 关闭优化)和-server重大区别在于jit优化关闭和开启

原因接着往下看

JVM运行时数据区

JVM运行时数据区

再看线程与线程之间读写的一张图

备注:(内存结构)运行时数据区———JVM java语言——— 工作内存和主内存

工作内存不仅仅是JVM,还包括cpu高速缓存

所以CPU等其他缓存,导致可见性(短时间内) , 但这一定不是主要原因,cpu缓存不会造成长时间不可见。

接着看其他可能原因

指令重排序

​ ——— 在不改变程序运行结果的前提下,调整代码运行顺序

JVM运行模式 :编译、解释、混合

编译:字节码 — jit提前编译 – 汇编

解释:字节码 – 一段段编译 – 汇编

混合:-运行的过程中,JIT编译器生效,针对热点代码进行优化

看下官方文档给的优化案例(orcale文档)

优化是针对单CPU

看下demo里的thread1

1
2
3
4
5
6
7
8
9
10
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (demo1.flag){
i++;
}
System.out.println(i);
}
});

对于这个线程来说,只有读,JIT优化器——— 循环读——— 读一次,所以我们上面while里面的代码在被优化后,变成了

1
2
3
4
5
6
7
8
9
10
11
while (demo1.flag) {
i++;
}
// 优化后的伪代码 --- 1. 旁敲侧击 2. 查看优化的汇编代码jitwatch
boolean f = demo1.flag;
if(f){
while(true) {
i++;
}
}

反思:这样明明会导致出错,为什么还要有JIT优化?

线程就出现了所见非所得,运行结果不可预测。所以就有了内存模型的概念,只要程序的所有执行产生的结果都可以由内存模型预测,内存模型决定了再程序的每个点上可以读取什么值。

JVM运行时数据区设计,多个内存区间进行交互势必会有问题,Java语言 提出 内存模型的相关规范

共享变量描述:可以在线程之间共享的内存称为共享内存或堆内存。所有实例字段、静态字段和数组元素都存储在堆内存中

volatile能禁止重排序,JIT不能改变语法,从而达到可见性的目的。

synchronized(加锁)后,为什么也能保证数据一致性?因为加锁就意味着在synchronized里面的代码块执行的时候是单线程,没有数据竞争,那么程序的所有执行看起来都是顺序一致的。

Happens-before  先行发生原则(先发生先生效):主要用于强调两个有冲突的动作之间的顺序,以及定义数据争用的发生时机。比如加了volatile关键字的变量就满足这个原则。

Volatile关键字

可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。

​ 根据JMM中规定的happen before 和同步原则:

​ 对某个volatile字段的写操作happens-before每个后续对该volatile字段的读操作

​ 对volatile变量v的写入,与所有其他线程后续对v的读操作

要满足这些条件,所有volatile关键字就有这些功能:

1:禁止缓存

​ volatile变量的访问控制符会加个c(反编译class文件能看出来)

从官方文档看下ACC_VOLATILE的含义

ACC_VOLATILE可保证立马可见,原因就是没有缓存。

2:对volatile变量相关的指令不做重排序