Java 中synchronized关键字及volatile的可见性实现

JMM(JAVA内存模型)

在这里插入图片描述
JMM工作原理如上图所示,一些被定义的变量都存放在主内存中,当一个线程想要修改一个变量的值时,那么这个变量会从主内存中拷贝到线程的工作内存(CPU缓存)中。之后线程对变量值做了更改,又会重新拷贝回主内存中。大家通过描述也可以看出来这些操作是分步执行的,这样就无法保证可见性和原子性。对于这种情况java也给出了很多解决办法,其中就有synchronized以及volatile

synchronized

JMM对于synchronized有两条规定:

  • 线程解锁前,必须把共享变量的最新之刷新到主内存中

  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁需要时同一把锁)

这样就会导致synchronized代码块是按照下面的顺序执行的:

  • 获得互斥锁,清空工作内存并从主内存拷贝变量的最新副本到工作内存

  • 执行代码

  • 将更改后的共享变量的值刷新到主内存

  • 释放互斥锁

正是上面的执行顺序使得synchronized具备内存可见性。显示锁(Lock)和synchronized有相同的内存可见性语义,其实原理跟synchronized类似。

volatile

为了实现 volatile 的可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

下面是基于保守策略的 JMM 内存屏障插入策略:

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。

  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。

  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。

  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
    下面是保守策略下,volatile 写操作 插入内存屏障后生成的指令序列示意图:
    在这里插入图片描述
    下面是在保守策略下,volatile 读操作 插入内存屏障后生成的指令序列示意图:
    在这里插入图片描述

上述 volatile 写操作和 volatile 读操作的内存屏障插入策略非常保守。在实际执行时,只要不改变 volatile 写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

对于volatile是如何实现内存可见性,深入来说:是通过加入内存屏障和禁止重排序优化来实现的。(重排序指单线程中在保证执行结果不变的前提下java虚拟机为了提升处理速度可能会将指令重排,达到最合理化)

对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,执行效果:

  • 改变线程工作内存中的volatile变量副本的值

  • 将改变后的副本的值从工作内存刷新到主内存

对volatile变量执行读操作时,会在读操作前加入一条load屏障指令,执行效果:

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

简单来说:volatile变量在每次被线程访问时,都强迫从sy主内存中重读变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样在任何时刻,不同的线程总能看到该变量的最新值。从而保证了变量的内存可见性。

synchronized和volatile的比较

  • volatile不需要加锁,比synchronized更加轻量级,不会阻塞线程

  • 从内存可见性讲,volatile读相当于加锁,volatile写相当于解锁

  • synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性

发布了92 篇原创文章 · 获赞 4 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/DingKG/article/details/103323162