阅读 54

Java并发编程之volatile关键字解析

此文章已同步更新至我的个人博客simonting.gitee.io


前言

在并发编程中,我们主要围绕着以下三个问题:

  • 原子性问题:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 可见性问题:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性问题:程序执行的顺序按照代码的先后顺序执行

要想保证并发程序能正确的运行,必须要保证以上三个问题,只要有一个没保证,就可能会发生错误。

JAVA内存模型

Java内存模型规定所有的变量存储在主内存中,每个线程都有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不是直接对主内存进行操作。并且每个线程不能访问其他线程的工作内存。

——这也就导致了线程之间的不可见性问题。

指令重排序

Java代码在编译后会编程Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行。

为了尽可能的减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的规则对代码顺序打乱,即程序运行的顺序可能跟我们编写的顺序不一样,目的是为了充分的利用CPU的性能,提高程序执行效率和速度,但是虚拟机会保证程序运行的结果与代码顺序执行的结果是一致的。

但是上面所说的仅仅针对单线程下,在多线程下,指令重排序就可能会导致程序打乱运行的结果与我们预想的结果不一致。

——这也就引出了有序性问题

volatile的内存语义

  • 保证线程之间的可见性;——可见性
  • 禁止指令重排序优化;——有序性
  • 对单个volatile变量的读/写操作具有原子性。——原子性

需要注意的是,volatile对类似于i++这种复合读写操作不具有原子性。

volatile如何实现线程之间的可见性?

当一个变量被volatile修饰时,当变量被一个线程修改之后,修改之后的新值对其他线程来说是立即感知的。因为volatile修饰之后会强制将修改之后的值立即写入主内存,且会通知其他线程它们工作内存中的缓存变量无效,这样其他线程就会在使用此变量时去主内存重新读取,此时读取的值就是正确的、最新的值。

synchronized也能保证可见性,但是volatile的执行成本更低,因为它不会引起线程上下文的切换和调度。

volatile如何实现禁止指令重排序优化(保证有序性)?

编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的指令重排序。

下面这段话摘自《深入理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也叫内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

synchronized也能保证有序性,原理是加锁。

参考

《深入理解Java虚拟机》

《Java并发编程的艺术》

博客:Java并发编程:volatile关键字解析