Synchronized与Volatile
标签:Java基础

Synchronized与Volatile

  1. 编译器优化:对于没有数据依赖关系的操作,编译器在编译的过程中会进行一定程度的重排。

    大家仔细看看线程 1 中的代码,编译器是可以将 a = 1 和 x = b 换一下顺序的,因为它们之间没有数据依赖关系,同理,线程 2 也一样,那就不难得到 x == y == 0 这种结果了。

  2. 指令重排序:CPU 优化行为,也是会对不存在数据依赖关系的指令进行一定程度的重排。

    这个和编译器优化差不多,就算编译器不发生重排,CPU 也可以对指令进行重排,这个就不用多说了。

  3. 内存系统重排序:内存系统没有重排序,但是由于有缓存的存在,使得程序整体上会表现出乱序的行为。

    假设不发生编译器重排和指令重排,线程 1 修改了 a 的值,但是修改以后,a 的值可能还没有写回到主存中,那么线程 2 得到 a == 0 就是很自然的事了。同理,线程 2 对于 b 的赋值操作也可能没有及时刷新到主存中。


    更多信息:

    1. Java的并发编程中的多线程问题到底是怎么回事儿?
    2. 再有人问你Java内存模型是什么,就把这篇文章发给他。
    3. 深入理解多线程(四)—— Moniter的实现原理
    4. 深入理解多线程(五)—— Java虚拟机的锁优化技术

Synchronized


一个线程在获取到监视器锁以后才能进入 synchronized 控制的代码块,一旦进入代码块,首先,该线程对于共享变量的缓存就会失效,因此 synchronized 代码块中对于共享变量的读取需要从主内存中重新获取,也就能获取到最新的值。

退出代码块的时候的,会将该线程写缓冲区中的数据刷到主内存中,所以在 synchronized 代码块之前或 synchronized 代码块中对于共享变量的操作随着该线程退出 synchronized 块,会立即对其他线程可见(这句话的前提是其他读取共享变量的线程会从主内存读取最新值)。

因此,我们可以总结一下:线程 a 对于进入 synchronized 块之前或在 synchronized 中对于共享变量的操作,对于后续的持有同一个监视器锁的线程 b 可见。虽然是挺简单的一句话,请读者好好体会。

注意一点,在进入 synchronized 的时候,并不会保证之前的写操作刷入到主内存中,synchronized 主要是保证退出的时候能将本地内存的数据刷入到主内存。


更多信息:再有人问你synchronized是什么,就把这篇文章发给他。

Volatile

volatile 的内存可见性

我们还是用 JMM 的主内存本地内存抽象来描述,这样比较准确。还有,并不是只有 Java 语言才有 volatile 关键字,所以后面的描述一定要建立在 Java 跨平台以后抽象出了内存模型的这个大环境下。

还记得 synchronized 的语义吗?进入 synchronized 时,使得本地缓存失效,synchronized 块中对共享变量的读取必须从主内存读取;退出 synchronized 时,会将进入 synchronized 块之前和 synchronized 块中的写操作刷入到主存中。

volatile 有类似的语义,读一个 volatile 变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个 volatile 属性会立即刷入到主内存。所以,volatile 读和 monitorenter 有相同的语义,volatile 写和 monitorexit 有相同的语义。

volatile 的禁止重排序

大家还记得之前的双重检查的单例模式吧,前面提到,加个 volatile 能解决问题。其实就是利用了 volatile 的禁止重排序功能。

volatile 的禁止重排序并不局限于两个 volatile 的属性操作不能重排序,而且是 volatile 属性操作和它周围的普通属性的操作也不能重排序。

之前 instance = new Singleton() 中,如果 instance 是 volatile 的,那么对于 instance 的赋值操作(赋一个引用给 instance 变量)就不会和构造函数中的属性赋值发生重排序,能保证构造方法结束后,才将此对象引用赋值给 instance。

根据 volatile 的内存可见性和禁止重排序,那么我们不难得出一个推论:线程 a 如果写入一个 volatile 变量,此时线程 b 再读取这个变量,那么此时对于线程 a 可见的所有属性对于线程 b 都是可见的。

volatile 小结

  1. volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值。在并发包的源码中,它使用得非常多。
  2. volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
  3. volatile 只能作用于属性,我们用 volatile 修饰属性,这样 compilers 就不会对这个属性做指令重排序。
  4. volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
  5. volatile 提供了 happens-before 保证,对 volatile 变量 v 的写入 happens-before 所有其他线程后续对 v 的读操作。
  6. volatile 可以使得 long 和 double 的赋值是原子的,前面在说原子性的时候提到过。

更多信息:深入理解Java中的volatile关键字

final 关键字

用 final 修饰的类不可以被继承,用 final 修饰的方法不可以被覆写,用 final 修饰的属性一旦初始化以后不可以被修改。当然,我们不关心这些段子,这节,我们来看看 final 带来的内存可见性影响。

之前在说双重检查的单例模式的时候,提过了一句,如果所有的属性都使用了 final 修饰,那么 volatile 也是可以不要的,这就是 final 带来的可见性影响。

在对象的构造方法中设置 final 属性,同时在对象初始化完成前,不要将此对象的引用写入到其他线程可以访问到的地方(不要让引用在构造函数中逸出)。如果这个条件满足,当其他线程看到这个对象的时候,那个线程始终可以看到正确初始化后的对象的 final 属性。

  • 6 min read

CONTRIBUTORS


  • 6 min read