Java并发编程基础-Java内存模型、volatile关键字与指令重排序及happens-before原则

Java并发编程实战学习笔记

 

目录

1.什么是Java内存模型?它和JVM内存模型有什么区别?

2.Java内存模型有哪些组成部分?

3.volatile关键字的作用?

4.什么是指令重排序

5.指令重排序必须满足什么条件

6.指令重排序会对多线程造成影响吗

7.什么是happens-before关系

8.happens-before和as-if-serial语义之间的区别

9.happens-before规则

8.双重检查加锁下的重排序问题


 

1.什么是Java内存模型?它和JVM内存模型有什么区别?

Java内存模型的作用是控制Java线程之间的通信,它决定了一个线程对共享变量的写入什么时候对另一个线程可见;而JVM内存模型指的是Java虚拟机运行时的内存分区。

2.Java内存模型有哪些组成部分?

简单地讲,Java内存模型约定线程之间共享的变量存储在主内存中,而每个线程又有自身私有的本地内存(工作内存),本地内存是Java线程直接能读/写到的区域。 

3.volatile关键字的作用?

  1. 1)在对变量进行写入时,会先写入到本地内存然后立即刷新到主内存中; 2)在对变量读取时,直接从主内存读取。
  2. volatile标识的共享变量不会被指令重排序。  

4.什么是指令重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列的执行顺序进行重新排列的一种手段。

5.指令重排序必须满足什么条件

  • as-if-serial语义即无论怎么样重排序,都不能够改变单线程程序运行的结果。例如:
 double a = 1.0; // 1
 double b = 2.0; // 2
 double result = a * b; // 3

经过指令重排序后,可能被优化成以下:

double b = 2.0; // 2
double a = 1.0; // 1
double result = a * b; //3

1和2的执行顺序被调换,但是不会影响单线程下,执行3获得的结果。

  • 数据依赖性即无论怎么样重排序,都不能够改变具有数据依赖性的两个操作的执行顺序

数据依赖性:如果两个操作同时访问一个变量,且这两个操作中有一个为写操作,那么这两个操作具有数据依赖性。

double a = 1.0; // 1
double b = a * 2.0; // 2
double result = a * b;

例如上述代码,不会被重排序调换1和2的位置,因为这两条指令都对a进行了操作,且1对a进行了写操作,存在数据依赖性。

6.指令重排序会对多线程造成影响吗

会。因为指令重排序只保证在单线程下的结果的一致性,但是在多线程下,若共享变量不存在数据依赖性,那么可能会造成不一样的结果,如以下代码:

    int a = 0;
    boolean flag = false;
    
    public void writer() {
        a = 1;          // 1
        flag = true;   // 2
    }
    
    public void reader() {
        if (flag) { // 3
            int i = a * a; // 4
            System.out.println("读取到的a为" + a); 
        }
    }

正常顺序执行过程,应该是1->2->3->4,读取到的a为1; 在上述代码中,由于a和flag不存在数据依赖性,所以指令1和2可能会被重排序。

在单线程环境下,由于as-if-serial语义,虽然执行顺序变成了2在1前面,但是仍然会得到一样的结果:

2->1->3->4,读取到的a为1

在多线程环境下,可能会发生以下问题

线程A执行writer(): 2->    ->1
线程B执行reader():    3->4     ,读取到的a为0

上述问题,只需要在a和b之前使用volatile修饰就可以解决了,因为使用volatile可以避免指令的重排序。那么执行顺序只能是1->2->3->4。

7.什么是happens-before关系

  • 定义:

happens-before关系即先行发生关系,它提供了线程操作可见性的保证。

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个之前。
  2. 如果两个操作之间存在happens-before关系,并不意味着Java平台的具体实现需要按照happens-before关系指定的顺序来执行。

8.happens-before和as-if-serial语义之间的区别

  1. as-if-serial语义保证了单线程内程序的执行结果不会被重排序改变。
  2. happens-before关系保证了正确同步的多线程程序的执行结果不会被改变。

9.happens-before规则

happens-before有如下规则用以保证它的先行发生可见性:

  1. 程序顺序规则:一个线程中的每个操作都happens-before后续操作。
  2. volatile变量规则:对一个volatie变量的写都happens-before后续的读。
  3. 传递性规则:如果A happens-before B,B happens-before C,那么A happens-before C。
  4. 监视器规则:如果A获取到了锁,那么B需要在A释放锁后方可获取锁。
  5. 线程启动原则: A线程开启了B线程,那么A线程在调用start()方法之前的代码 happens-before B线程。
  6. 线程终止原则:A线程开启了B线程,且A线程等待B线程的执行(b.join()),那么B线程所有操作都happens-before A。

8.双重检查加锁下的重排序问题

一个错误的双重检查加锁Demo

public class Singleton {
    private Singleton instance = null;
    private Singleton() {}
    
    public Singleton getInstance() {
        // 先判断对象是否已经初始化
        if (instance == null) {
            synchronized (instance) {
                // 确定对象没有初始化
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

上述代码发生错误的原因在于return new Singleton()这行代码,这行代码并不是一个原子性的的操作,它可以分为以下三个阶段:

1.分配对象内存
2.初始化对象
3.将对象引用到内存空间

其中,在将对象引用到内存空间时,instance就不等于null了。

在单线程情况下,2和3指令发生重排序,但是由于as-if-serial语义,其执行结果也是不变的:

 

 

在多线程情况下,有可能会发生以下的执行顺序,导致程序执行结果出错: A线程在执行到第3步的时候即设置instance指向内存空间,此时B线程判断其不为空,返回instance,但此时instance并未进行初始化,所以出现了错误,返回了一个空对象。

 

解决该问题的方法是在instance前加上volatile修饰,这样就能禁止重排序。 

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员不鸣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值