java 内存模型与 volatile 的实现

2018-07-26 16:18:38   最后更新: 2018-07-26 16:20:35   访问数量:99




此前的日志中,我们介绍了线程同步机制

java 的线程安全性与线程同步机制

我们提到了 volatile、synchronized 关键字与 java.util.concurrent.locks.ReentrantLock 类,本篇日志我们来详细讲解一下 volatile 的用法

 

要想深入理解以上提到的并发环境同步工具的意义和用法,就必须先深入了解 java 的内存管理,即 JMM (java memory model),这是 java 最核心的设计理念

众所众知,现代计算机存储设备的读写性能与处理器的运算性能有几个数量级的差距,所以现代计算机不得不引入多级高速缓存,来作为内存与CPU之间的缓冲,这虽然解决了处理器与内存处理速度的矛盾,但也为计算机系统带来巨大的复杂度,如何保证缓存一致性成为了一个新引入的问题,当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,为了解决这个问题,不同的软件系统规定了不同的协议,Java 虚拟机规范中就定义了 JMM 规范来解决各种存储访问带来的问题,JDK1.5 实现的 JSR-133 协议是相对非常成熟和完善的

对于线程间共享的变量,如上述所说,在线程并发环境中可能产生并发条件,JMM 规定,java 虚拟机将存储设备抽象为主内存(Main Memory)与工作内存(Working Memory),所有的变量都存储在主内存中,每条线程都拥有自己的工作内存,与处理器高速缓存类似,线程的工作内存中保存被该线程使用到的变量主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,不同的线程也无法相互访问对方的工作内存,线程间数据的传递必须通过主内存来完成

JMM 定义了内存的八种基本操作,并且要求他们必须是原子性的

JMM 内存的八种操作
操作名称作用域描述
lock锁定主内存将一个变量标识为线程独占
unlock解锁主内存释放标识为线程独占的变量为全局可见
read读取主内存将一个变量的值从主内存传输到线程工作内存中
load载入工作内存将 read 操作从主内存中得到的变量放入工作内存变量副本中
use使用工作内存将工作内存中的变量值传递给执行引擎,让虚拟机可以使用该变量
assign赋值工作内存将一个执行引擎接受到的值赋值给工作内存中的变量
store存储工作内存把工作内存中的一个变量的值传递到主内存
write写入主内存将 strore 操作后工作内存传递到主内存的变量的值写入主内存的变量中

 

以上八个操作中,read 与 load、store 与 write 分别必须是成对出现的,不允许单独出现,也不允许任何一个线程丢弃他最近 assign 操作后的变量值,同时,assign 是唯一出发线程工作内存同步回主内存的动作

所有的变量,都必须在主内存中诞生和初始化,也就是说,对一个变量第一次执行 use 和 store 操作前必须先执行 assign 与 load 操作

这些复杂的原则就是先行发生原则(happens-before)的一部分

 

 

先行发生原则是一系列规则的总和,它规定了 java 虚拟机必须遵循的内存模型的顺序规则,根据这些规则,可以很轻易的判断出两个操作是否有顺序保障

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
  3. volatile变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
  5. start规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作
  6. join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

 

说到这里,其实就不需要对 volatile 关键字进行解释了,JRS 中已经明确规定了 volatile 变量的规则

  • volatile变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读

 

当一个变量被定义为 volatile 之后,他将具备两种特性:

  1. 保证此变量对所有线程都可见
  2. 任何一个线程修改了该变量的值,其他线程将立即得知该新值

volatile 变量是通过线程的每次读取都强制从主内存刷新到工作内存实现的,同时,所有更改都强制立即从工作内存同步到主内存中,因此,volatile 变量并不能保证其并发安全性

 

显而易见,使用 volatile 的代码复杂度要比使用锁简单得多,而通常 volatile 变量的同步机制要比锁的性能更高一些,尤其在读操作远远多于写操作的环境中,这是因为 volatile 变量的写操作性能要低一些

以下五个场景是 volatile 非常适合的应用场景:

 

状态标志

在多线程环境中,某个线程为主线程或调度线程,只有该线程可以更改状态标志,从而实现对其他线程的调度和控制,所有工作线程读取状态标志来判断当前所需要执行的工作

由于 volatile 能够保证每次读取到的值都是强制从主内存刷新到工作内存中的值,从而保证了所有工作线程都能够立即读取到正确的指令

 

一次性安全发布

考虑下面一段代码:

public class BackgroundFloobleLoader { public Flooble theFlooble; public void initInBackground() { // do lots of stuff theFlooble = new Flooble(); // this is the only write to theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // do some stuff... // use the Flooble, but only if it is ready if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } }

 

上面的代码中,假设两个类分别在不同的线程中执行,那么 SomeOtherClass 中 doSomething 方法获取到的对象有可能是未完全初始化完成的对象,从而可能带来完全无法预知的错误出现

将 BackgroundFloobleLoader 类的 theFlooble 成员设置为 volatile 就可以避免并发环境中读取到初始化了一半的对象的问题了

 

独立观察

对于统计信息的收集程序,往往收集线程需要将信息发布出来,其他线程需要获取到最新的数据

比如天气情况的收集,这个保存天气情况的值或对象是实时变化的,volatile 保证了他是最新的

 

volatile bean

@ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }

 

上述代码中的类是一个应用于并发环境的 JavaBean,他可以保证任何成员读取的正确性

 

计数操作

++x 操作实际上是三种操作的组合:读、加1、存储,因此他是非线程安全的

如果多个线程试图同时使用计数器,那么必须通过 volatile 保证读取的准确性,通过加锁实现整个操作的原子性

@ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } }

 

 

同时满足下面的两个条件,可以通过 volatile 来保证线程安全性,如果不满足,就必须要使用锁来保证并发环境下的安全了

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中

 

《深入理解Java虚拟机 —— JVM高级特性与最佳实践》

https://www.ibm.com/developerworks/cn/java/j-jtp06197.html

 






技术帖            java      synchronized      jmm      jsr      技术分析      volatile      线程安全     


京ICP备15018585号