JVM学习笔记之八(JMM)

13 篇文章 0 订阅
1 篇文章 0 订阅

内存模型(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;
    }
}

为什么呢?分析:

  1. 初始状态,t 线程刚开始从主存读取了 run 的值到工作内存
    在这里插入图片描述
  2. 因为 t 线程要频繁从主内存读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存 run 的访问,提高效率
    在这里插入图片描述
  3. 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
$ 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

变量都是指成员变量或静态成员变量

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值