JVM学习笔记——内存模型篇( 二 )

内存模型之可见性我们将在下面仔细介绍可见性的特点
可见性介绍首先我们简单介绍一下可见性的定义:

  • 我们需要保证 , 在多个线程中,对同一变量的修改需要被其他线程所知道并且可以调用
可见性的注意点:
  • 我们的程序往往具有自动优化,对于多次取同一值的数据可能会封装在自己的程序中而不是在源程序读取,这就会导致可见性失效
可见性问题我们同样给出一段代码作为可见性的案例:
package cn.itcast.jvm.t4.avo;public class Demo4_2 {static boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){}});t.start();Thread.sleep(1000);run = false; // 线程t不会如预想的停下来}}我们的运行结果如下:
// 我们上述代码希望:程序在执行1s后停止运行,但我们的程序却一直运行不会停止...可见性分析首先我们回顾开头的注意点:
  • 程序具有自身很多的优化步骤,可能哪一步就会导致我们的程序出错
我们来简单分析:
  1. 初始状态,t 线程刚开始从主内存读取了 run 的值到工作内存 。

JVM学习笔记——内存模型篇

文章插图
  1. 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问

JVM学习笔记——内存模型篇

文章插图
  1. 1 秒之后,main 线程修改了 run 的值 , 并同步至主存 , 而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

JVM学习笔记——内存模型篇

文章插图
可见性实现我们的可见性经常通过一种修饰词来实现:
  • volatile(易变关键字)
  • 它可以用来修饰成员变量和静态成员变量
  • 他可以避免线程从自己的工作缓存中查找变量的值 , 必须到主存中获取它的值 , 线程操作 volatile 变量都是直接操作主存
同时我们给出另一种方法:
  • synchronized 语句块
  • synchronized既可以保证代码块的原子性,也同时保证代码块内变量的可见性
  • 但缺点是synchronized是属于重量级操作,性能相对更低
我们如果修改之前代码,就可以采用volatile修改:
package cn.itcast.jvm.t4.avo;public class Demo4_2 {static volatile boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){}});t.start();Thread.sleep(1000);run = false; // 线程t不会如预想的停下来}}内存模型之有序性【JVM学习笔记——内存模型篇】我们将在下面仔细介绍有序性的特点
有序性介绍首先我们简单介绍一下有序性的定义:
  • 有序性就是指我们底层代码实现的具体顺序,在正常情况下是按正常顺序执行
有序性的注意点:
  • 同样底层也会进行部分优化,对于有序性的优化常常被称为指令重排 , 是指在不影响操作的前提下进行语句的优化调整
有序性问题我们同样给出一段代码:
int num = 0;boolean ready = false;// 线程1 执行此方法public void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}} // 线程2 执行此方法public void actor2(I_Result r) {num = 2;ready = true;}我们下面会给出其所有情况:
// 具体分为四种情况,前三种属于正常的多线程无锁导致的情况 // 情况1:线程1 先执行 , 这时 ready = false,所以进入 else 分支结果为 1// 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1// 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)// 但是第四种!却是因为代码重排所导致的情况:有序性分析首先我们在重新介绍一下指令重排:
  • JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现
我们可以给出结果为0的执行顺序:
线程2:ready = true;(由于操作更加简单 , 导致JIT将它放在前面编译)线程1:if判断 true线程1:r.r1 = num + num;(此时num为0),结果r1=0JVM 会在不影响正确性的前提下,可以调整语句的执行顺序:
// 下面是模拟情况:static int i;static int j;// 在某个线程内执行如下赋值操作// i为较为耗时的操作,j为简单操作i = ...;j = ...;// 底层代码会认为i和j的赋值操作毫无关系,他们谁先执行都可以,所以会优先执行简单的操作// 所以我们的代码可能变为:static int i;static int j;j = ...;i = ...;

推荐阅读