内存模型(JMM)
VM 和 JMM 的区别
- java虚拟机模型( JVM )
jvm的内部结构如下图所示,这张图很清楚形象的描绘了整个JVM的内部结构,以及各个部分之间的交互和作用。
- Java内存模型(Java Memory Model,JMM)JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
1.1 原子性
- 原子性在学习线程时讲过,下面看一个例子
- 问题提出,两个线程对初始值为 0 的静态的变量一个自增、一个自减,各做 50000 次 结果是 0 吗?(不一定,实验需要做多次,有可能是 0 )
public class JMM {
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int j = 0; j < 50000; j++) {
i++;
}
});
Thread t2 = new Thread(()->{
for (int j = 0; j <50000; j++) {
i--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
1.2 问题分析
- 以上结果可能为正、负、0,什么呢?因为 Java 中对静态变量的自增、自减并不是原子操作。
- 例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
0: getstatic #2 // Field i:I
3: iconst_1 // 准备常量1
4: iadd // 加法
5: putstatic #2 // Field i:I
- i-- 而言
0: getstatic #2 // Field i:I
3: iconst_1
4: isub
5: putstatic #2
- JVM 内存模型如下,完成静态变量的自增、自减需要在主存和线程内存中进行数据交换:
如果单线程以下 8 行代码是顺序执行的不会储问题
0: getstatic #2 // Field i:I
3: iconst_1 // 准备常量1
4: iadd // 加法
5: putstatic #2 // Field i:I
=================================
0: getstatic #2 // Field i:I
3: iconst_1
4: isub
5: putstatic #2
但是,多线程就会出现交错运行,就会出现正负数的问题了
1.3 解决问题
- synchronized (同步关键字)
synchronized (对象){
代码块
}
2. 可见性
2.1 退不出的循环
- 先看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止
public class Main2 {
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t= new Thread(()->{
while (run){
//System.out.println("run..."); // 加了这一句就不会有效果了,因为缓存原因
}
});
t.start();
Thread.sleep(1000);
run = false;
}
}
为什么呢?分析:
- 初始状态,t 线程刚开始从主存读取了 run 的值到工作内存
- 因为 t 线程要频繁从主内存读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存 run 的访问,提高效率
- 1 秒后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存的高速缓存中读取这个变量的值,结果永远是旧的值
2.2 解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态变量,他可以避免线程从自己的工作缓存查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存的
2.3 可见性
- 前面例子体现了可见性,它保证的是在多少线程之间,一个线程对 volatile 变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程情况(不能用在之前的 i++ 跟 i-- 情况):
从字节码层面看:
getstatic run // t 获取 run
getstatic run // t 获取 run
getstatic run // t 获取 run
getstatic run // t 获取 run
putstatic run // main 修改 run 仅一次
getstatic run // t 获取 run
public class Main2 {
private static volatile boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t= new Thread(()->{
while (run){
//System.out.println("run..."); // 加了这一句就不会有效果了,因为缓存原因
}
});
t.start();
Thread.sleep(1000);
run = false;
}
}
注意
synchronized 语句块既可以保证原子性也可以保证可见性,但是缺点是属于重量级操作,性能相对低
上面代码中 **System.out.println(“run…”)**为什么就能保证可见性呢?看看源码
public void println(String x) {
synchronized (this) { // 使用了 synchronized 关键字
print(x);
newLine();
}
}
3. 有序性
3.1 诡异的结果
public class Main {
private static int num = 0;
private static boolean ready = false;
// 线程 1 执行
public static void actor1(IResult result){
if (ready){
result.setR1(num + num);
}else {
result.setR1(1);
}
}
// 线程 2 执行
public static void actor2(){
num = 2;
ready = true;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
IResult iResult = new IResult();
Thread t1 = new Thread(()->{
actor1(iResult);
});
Thread t2 = new Thread(Main::actor2);
t1.start();
t2.start();
t1.join();
t2.join();
num = 0;
System.out.println(iResult.getR1());
}
}
}
class IResult {
private int r1;
public int getR1() {
return r1;
}
public void setR1(int r1) {
this.r1 = r1;
}
}
==============结果=========================
有 0 4 1
- 这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化买这个现象需要通过大量测试才能复现:
- 借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress
$ mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jcstress \
-DarchetypeArtifactId=jcstress-java-test-archetype \
-DgroupId=org.sample \
-DartifactId=test \
-Dversion=1.0
3.2 解决
使用 volatile 修饰变量,可以禁止指令重排
3.3 有序性理解
- 同一个线程内,JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码:
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; // 较为耗时操作
j = ...;
- 可以看到,至于是先执行 i 还是先执行 j,对最终结果不会产生影响。所以,上面代码真正执行时,可以是:
i = ...; // 较为耗时操作
j = ...;
================================
j = ...;
i = ...; // 较为耗时操作
- 这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性,例如著名的 double-check-locking 模式实现单例
public class Singleton {
private Singleton(){}
public static Singleton INSTANCE = null;
public static Singleton getInstance(){
// 实例没创建,才会进入内部的 synchronized 代码块
if (INSTANCE == null){
synchronized (Singleton.class){
// 也许其他线程已经创建实例
if (INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
以上实现的特点:
-
懒惰实例化
-
首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
-
但在多线程环境下,上面的代码是有问题的, INSTANCE = new Singleton() 对应的字节码为:
0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "<init>":()V
7: putstatic #4 // Field INSTANCE:Lcn/itcast/jvm/t4/Singleton;
- 其中 4 7 两步的顺序不是固定的,也许 JVM 会优化为:先将引用地址赋值给 INSTANCE 变量后,再执行构造方法,如果两个线程 t1、t2 按如下时间执行
时间1 t1 线程执行到 INSTANCE = new Singleton();
时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0 处)
时间3 t1 线程将引用地址赋值给 INSTANCE,这时 INSTANCE != null(7 处)
时间4 t2 线程进入getInstance() 方法,发现 INSTANCE != null(synchronized块外),直接
返回 INSTANCE
时间5 t1 线程执行Singleton的构造方法(4 处)
- 这时 t1 还没完全将构造方法下执行完毕,如果在构造方法中执行很多初始化操作,那么 t2 拿到的将是一个未初始化完毕的单例
- 对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才
会真正有效
3.4 happens-before
- happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,
抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变
量的读可见 - 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
public class Main {
static int x;
static Object m = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start();
}
}
- 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
public class Main {
private volatile static int x;
public static void main(String[] args) {
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
}
}
- 线程 start 前对变量的写,对该线程开始后对该变量的读可见
public class Main {
private static int x;
public static void main(String[] args) {
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
}
}
- 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或
t1.join() 等待它结束)
public class Main3 {
private static int x;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
}
}
- 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通
过t2.interrupted 或 t2.isInterrupted)
public class Main {
private static int x;
public static void main(String[] args) throws InterruptedException {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
}
- 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
- 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z
变量都是指成员变量或静态成员变量