JMM内存模型-包括(指令重排序,volatile完整语义,happens-before原则等,带图)

写在前面:
一开始我是不知道JMM内存模型这个概念的,2年前面试的时候面试官问了我对java内存模型的理解,当时说了堆栈,还有GC,事后才知道回答错了,当时匆匆看了相关的博客准备下一场面试,也没有好好总结。最近通过看《Java并发编程的艺术》、《深入理解Java虚拟机》、JSR133,有了自己的理解,记录一下吧。
JMM-Java内存模型思维导图

一、Java内存模型基础

并发编程的两大难题,如何解决线程间的通信和同步?

  • 线程间如何通信
    通信是指线程如何交换自己的信息,在命令式编程中有两个解决方案。
    • 线程通过共线公共状态通过写-读的方式隐式交换信息。
    • 线程通过消息传递显示传递信息
  • 线程间如何同步
    • 在共线内存的编发模型里,方法或代码块需要显示指定在线程中互斥执行
    • 在消息传递的并发模型里,由于消息的发送必须在接收之前,同步是隐式的。

二、Java内存抽象结构

    在Java中,共享变量(包括实例字段、静态字段及构成数组对象的元素)都存储在主存中。每个线程有自己的工作内存,线程的工作内存中保存了主存的副本(并不是完全copy过来,例如一个对象有3M,只有用到元素才会保存副本),线程不能直接操作主存中的数据,必须先读/写本地线程工作内存中。
    volatile类型的变量有会有本地工作内存的副本,只是操作会立刻反应到主存中,看起来像直接操作主存一样。
Java内存抽象

三、从源代码到指令的重排序

  • 编译器重排
    编译器在不改变单线程语义的情况下,可以对指令进行重排。
  • 处理器重排
    现代处理器采用指令级并行技术,可以将多条指令折叠执行。如果不存在数据的依赖性,处理器可以改变语句对应机器执行的执行顺序
  • 内存系统重排序
    由于处理器会使用缓存和读/写缓冲区,有可能加载和存储操作看起来是乱序执行的。
    处理器、高速缓存、主内存之间的交互关系
        从源代码到实际的指令执行序列会经历以下过程:
    从源代码到执行的顺序
        上面的1属于编译器重排,2,3属于处理器重排。JMM尽可能的放松对重排的干预,保证cpu的性能得到极致的压榨,同时会禁止特定的编译器重排和处理器重排。

四、Happens-Before(先行发生)原则

    Happens-Before是JMM模型定义的两项操作之间的排序关系,如果操作A
先行发生于操作B,就是在操作B之前,A操作产生的影响一定能被B看到,影响包括,改变了某变量的值,调用了某个方法等。

    举个例子:

// 线程A操作
i = 1;

// 线程B操作
j = i;

// 线程C操作
i = 2;

    假设线程A先行发生于操作B(不考虑C),那就可以肯定B线程中的j一定等于1,因为A操作产生的影响能被一定能被B看到;考虑线程C呢,如果线程B和线程C没有先行发生关系,那么B的值就不确定了,因为B有可能看到C的操作,有可能看不到。

JMM向程序猿保证的线程发生关系,如果两个操作不在此列,则两个操作没有先行发生关系

  • 程序顺序规则:一个线程前面的每一个操作先行发生后面的操作,前面的改变对后面是立即可见的,虽然可能没有刷新到主存
  • 管程协定规则:一个unlock()操作先行发生于后面的lock操作,这个后面指的是是时间上的后面,必须是同一个lock。
  • volatile变量原则:一个volatile的写操作先行发生于对一个volatile的读操作
  • 传递性:A先行发生于B,B先行发生于C,则A先行发生于C
  • 线程启动规则:Thread#start()先行发生于这个线程的任何操作
  • 线程终止规则:线程的所有操作都先行发生于线程的终止操作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thead#interrupted()检测
  • 对象终结个规则:一个对象的初始化完成先行于它的 finalize()方法

Java能保证的先行发生关系只有这些。

    先行发生与时间先后的关系,一个操作A先行发生于操作B,操作A一定比操作B时间上先执行完吗?如果一个操作时间上比操作B时间上先完成,那么操作A一定先行发生于操作B吗?答案都是否定的。举个栗子:

// 在同一个线程中
//操作A
i = 1;
// 操作B
j = 2;

    由单线程顺序规则可以保证A先行发生于B,只需要要保证A操作的结果对B可见,但B对A是没有数据依赖的,可能会出现重排序,最后的指令可能会反过来执行,由此可见,先行发生前的操作不一定时间上先执行。

// 重排序影响
//先执行操作B
j = 2;
// 再执行操作A
i = 1;

    再看个例子:

private int v = 0;
    
    public int reader() {
        return v;
    }
    
    public void wirte(int newV) {
        this.v = newV;
    }

    假如 线程A先调用 wirte(1)方法,线程B调用reader()方法,线程B看到的一定是1吗,不一定的,因为JMM所有的先行发生规则都不适用,所有线程A的操作不保证对B可见。可以通过volatile规则让A操作先行发生于B操作(对volatile变量的写操作先行发生于对volatile的读操作)

private volatile int v = 0;
    
    public int reader() {
        return v;
    }
    
    public void wirte(int newV) {
        this.v = newV;
    }

    加上volatile之后可以保证线程B能够看到线程A修改的最新值。也可以套用管程规则(同一个锁的解锁操作先行发生于获取锁的操作),也能保证线程B看到的变量为最新值1。

private int v = 0;
    
    public synchronized int reader() {
        return v;
    }
    
    public synchronized void wirte(int newV) {
        this.v = newV;
    }

五、volatile语义

    JMM为volatile定义了一些特殊的规则,当一个变量被volatile修饰,它将具备以下的特性:

  1. 保证此变量对所有线程的可见性,一个线程对变量做了修改,保证新值对其他线程来说是立刻知道的,会立即刷新到主存中。当一个线程写volatile变量时,会是其他处理器缓存数据无效,根据处理器不同会锁总线或者部分缓存,修改完后解锁,同时将主存中最新数据加载过来。

可见性

下面的代码是线程安全的吗?

private volatile i = 0;

private void add() {
 	i++;
}

    答案是否定的,因为i++操作并不是原子的,至少包含下面三个操作:

int temp = i;
temp = i + 1;
i = temp;

    volatile能够保证 temp看到的i值是最新的,i = temp会立刻刷新到主存中,并且其他线程会立刻得知,但temp 的值不会从主存中读取,所以即使被 volatile修饰 i++,也不是安全的。

    下列场景适合用volatile:

  volatile boolean shutDown = false;
   
   void work() {
       
       while (shutDown) {
           // doSomeTing
       }
   }

    JMM能够保证shutDown一旦被修改为true,work会立刻停下来。

  1. 禁止指令重排优化

    普通变量能保证在该方法执行时,所有依赖到该变量的地方都能获取到正确的结果(编译器和处理器会为了提升效率,不改变单线程语义的情况下进行重排)。

    看个代码吧:

    boolean init = false;

    void work() {
        if (init) {
            // startWork
        }
    }

    void init() {

		// 先进行初始化
        doSomeThing();
        
        // 再将初始化标志写为ture
        init = true;
    }

    void doSomeThing() {

    }

     init方法可能会被重排序优如下;这样程序多线程就会出现问题了,因为实际上还没有进行初始化work()方法依赖初始化操作的方法就会执行错误了。为了解决这个问题,可以将init变量改为 volatile 变量。这样就能够保证 先执行初始化方法doSomeThing(),然后init 置为true,work方法也能再次运转正常了。

     void init() {


       // 先写初始化标志写为ture
        init = true;
        
		// 再进行初始化
        doSomeThing();
  
    }

    在旧的线程模型中,volatile的写-读没有释放锁-获取锁所具有的内存语义。
为了提供一种轻量级锁的功能,JSR-133的专家们增加了volatile的语义,严格禁止编译器和处理器对volatile变量和普通变量进行重排序,通过在指定位置插入内存屏障的方式禁止。

    一个volatile实现轻量级锁的demo,相信大家和我一样都看过一个题目:三个线程顺序打印ABC 10次

    第一个方案通过锁来实现:

 private static final Object monitor = new Object();

    private static int count = 0;

    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            while (count <= 30) {
                synchronized (monitor) {
                    if (count % 3 == 0) {
                        System.out.println("A");
                        count++;
                    }
                }
            }

        });


        Thread b = new Thread(() -> {
            while (count <= 30) {
                synchronized (monitor) {
                    if (count % 3 == 1) {
                        System.out.println("B");
                        count++;
                    }
                }
            }

        });


        Thread c = new Thread(() -> {
            while (count <= 30) {
                synchronized (monitor) {
                    if (count % 3 == 2) {
                        System.out.println("C");
                        count++;
                    }
                }
            }

        });

        c.start();
        b.start();
        a.start();

    }

     这样简单粗暴的synchronized锁自然是没问题的,下面通过volatile轻量级锁的语义来实现:


    private static volatile int count = 0;

    public static void main(String[] args) {

        Thread a = new Thread(() -> {
            while (count < 30) {
                if (count % 3 == 0) {
                    System.out.println("A" + "  " + count);
                    count++;
                }

            }

        });

        Thread b = new Thread(() -> {
            while (count < 30) {
                if (count % 3 == 1) {
                    System.out.println("B" + "  " + count);
                    count++;
                }

            }
        });

        Thread c = new Thread(() -> {
            while (count < 30) {
                if (count % 3 == 2) {
                    System.out.println("C" + "  " + count);
                    count++;
                }
            }
        });

        c.start();
        b.start();
        a.start();

    }

    程序分析,if (count % 3 == n)读取主存中最新的值,当不满足条件时自旋,volatile 语义会禁止
下面的语句重排:打印语句与count++(实际上时更底层的指令),同时写入操作立刻刷新到主存,其他打印线程停止自旋获取到锁开始执行打印语句。前面不是说volatile 下的i++不能保证多线程的安全性,这里count++会不会有问题呢,不会的啊,因为if() {}里面的语句已经被锁住了,没有多线程操作,这里的count++是线程安全的。

            while (count < 30) {
            	// 读取最新的值
                if (count % 3 == 2) {
                    System.out.println("C" + "  " + count);
                    count++;
                }
				// 自旋等待锁	
            }

    Lock接口 也是利用volatile语义实现了锁的功能,当然会比三个线程打印ABC更为复杂,关于Lock接口以及其它J.U.C包下的工具类,准备这个月在Java并发专栏下面记录。

六、synchronized 内存语义

// todo 2020-08-01待更新

七、final语义

// todo 2020-08-01待更新

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值