Java语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。
编写高质量代码 改善Java程序的151个建议 PDF高清完整版
Java对象值传递和对象传递的总结
volatile 写和读的内存语义:
线程 A 写一个 volatile 变量,实质上是线程 A 向接下来将要读这个 volatile 变量的某个线程发出了(其对共享变量所在修改的)消息。
线程 B 读一个 volatile 变量,实质上是线程 B 接收了之前某个线程发出的(在写这个 volatile 变量之前对共享变量所做修改的)消息。
线程 A 写一个 volatile 变量,随后线程 B 读这个 volatile 变量,这个过程实质上是线程 A 通过主内存向线程 B 发送消息。
JMM(java内存模型)的支持原理
volatile用来确保将变量的更新操作通知到其它线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量时共享的,因此不会将该变量的操作与其它内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其它处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。严格限制编译器和处理器对 volatile 变量与普通变量的重排序,确保 volatile 的写-读和锁的释放-获取具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要 volatile 变量与普通变量之间的重排序可能会破坏 volatile 的内存语义,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
volatile变量对可见性的影响比volatile变量本身更为重要。当线程A首先写入一个volatile变量并且线程B随后读取该变量时,在写入volatile变量之前对A可见的所有变量的值,在B读取了volatile变量后,对B也是可见的。因此,从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块。我们可以简单理解为当要读取一个volatile变量时总是伴随着先要到主内存刷新取到最新值,而写变量时会把值直接写到主内存以对其它线程可见,这些都是由JMM(java内存模型)禁止指令重排序做保证的。不建议过度依赖volatile变量提供的可见性。如果在代码中依赖volatile变量来控制状态的可见性,通常比使用锁的代码更脆弱,也更难理解。
volatile典型用法
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保他们所引用对象的状态的可见性,以及标示一些重要的程序声明周期事件的发生,如初始化或关闭。
volatile boolean asleep;
...
while(!asleep)
countSomeSheep();
上面是volatile变量的一种典型用法:检查某个状态标记以判断是否退出循环。在这个示例中,线程试图通过类似于数数的传统方法进入休眠状态。为了使这个示例能正确执行,asleep必须为volatile变量。否则,当asleep被另一个线程修改时,执行
判断的线程却发现不了。我们也可以用锁来确保asleep更新操作的可见性,但是这样将使代码变得更加复杂。
虽然volatile很方便,但也存在一些局限性。volatile变量通常用作某个操作完成发生中断的标志。尽管volatile变量也可以用于表示其他的状态信息,但在使用时要非常小心。例如volatile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
当且仅当满足以下所有条件时,才应该使用volatile变量。
a,对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
b,该变量不会与其它状态变量一起纳入不变形条件中。
c,在访问变量时不需要加锁。
参考文献: