在本文中,我想就此主题分享一种可能对您有所帮助的不同方法,并讨论有关的一些特殊性和误解volatile。关于此主题的材料非常好(例如“ Java Concurrency in Practice” [ JCIP ])。我鼓励您阅读它;)
我们将讨论volatile从Java 5开始定义的语义(在以前的Java版本中,volatile 没有内存一致性/状态可见性语义)。
Java并发:什么是Volatile?
简短答案:这volatile是一个关键字,我们可以将其应用于字段, 以确保当一个线程向该字段写入值时,写入的值对于随后读取该字段的任何线程“立即可用”(可见性功能)。
语境
当多个线程需要与某些共享数据进行交互时,需要考虑三个方面:
- 可见性:线程对共享数据执行操作的效果应由其他线程(一致的视图)观察到;
- 顺序:执行顺序应与源代码中出现的语句相同(顺序一致);
- 原子性:当另一个线程对共享数据执行某些操作时,任何线程都不应干预。
在没有必要的同步的情况下,编译器,运行时或处理器可能会应用各种优化方法,例如代码重新排序和缓存。这些优化可以与不正确同步的代码进行交互,这种方式在检查源代码时并不立即显而易见。即使语句按照其在线程中的出现顺序执行,缓存也可以防止最新值反映在主内存中。
为了防止错误或不可预测的结果,我们可以使用(某种形式的)同步。
值得一提的是,同步并不意味着使用synchronized关键字(它是基于内部的隐式/监视/内部锁)的;例如,锁定对象和volatile关键字也是同步机制。
在多线程上下文中,我们通常必须求助于显式同步。但是,在特定情况下,我们可以使用一些不依赖同步的替代方法,例如原子方法和不可变对象。
为了说明未正确同步程序的风险,让我们考虑从JCIP书籍中借来的以下示例。该程序可以简单地打印42,甚至可以打印0,甚至可以永久挂起!
为什么会发生这种情况?在Java中,默认情况下,假定一段代码仅由一个线程执行。正如我们所看到的,只要我们得到与原始代码相同的结果,编译器,运行时或处理器就可以优化它们。在多线程上下文中,开发人员有责任使用适当的机制来正确同步对共享状态的访问。
在这个特定的示例中,编译器可能会看到“ ready”为假(默认值),并且它永远不会改变,因此最终将导致无限循环。在另一方面,如果’准备好”的更新值是可见的ReaderThread 之前‘号’是(代码重新排序/高速缓存),ReaderThread会看到‘数量’为0(默认值)。
发生前的关系
“可以通过先发生后关系来命令两个动作。如果一个动作发生在另一个动作之前,那么第一个动作对于第二个动作是可见的 ,并且在第二个动作之前是有序 的(例如,不必在该线程的开始之前将默认值写入该线程构造的对象的每个字段中,因为只要没有人读过这个事实)。
更具体地说,如果两个动作共享事前发生关系,则它们不一定必须按照顺序与没有事前发生关系的任何代码发生了关系。”
此关系是通过以下任一方式建立的:
该synchronized构建体(这也提供了原子性);
该volatile结构;
Thread.start()和方法; Thread.join()
java.util.concurrent中所有类的方法及其子包;
等等。
要了解这种关系的重要性,让我们回顾一下数据竞争的概念。“当至少一个线程将变量写入并至少另一个线程将变量读取,并且读取和写入未按事前发生关系进行排序时,就会发生 数据争用。正确同步的程序是没有数据争用的程序”(从此处开始)。
更具体地说,正确同步的程序是其顺序一致的执行没有任何数据竞争的程序。
Volatile
volatile是一种轻量级的同步形式,可解决可见性和排序方面的问题。volatile用作字段 修饰符。这样做的目的volatile是确保当一个线程将值写入字段时,写入的值对于随后读取该值的任何线程“立即可用”。
关于volatile一方面与引用和对象之间的关系存在一个普遍的误解。在Java中,有对象 和对这些对象的引用。我们可以将引用视为对象的“指针”。所有变量(期望布尔值或long之类的基元)都包含引用(对象引用)。
volatile作用于原始变量或参考变量。volatile与(reference)变量所引用的对象之间没有任何关系。
声明一个共享变量以volatile 确保可见性。
volatile变量不会缓存在寄存器中,也不会缓存在对其他处理器隐藏的缓存中,因此对volatile变量的读取始终返回任何线程的最新写入。“volatile变量的可见性影响超出了变量本身的价值。当线程A写入volatile变量,然后线程B读取相同的变量时,在写入volatile变量之前A可见的所有变量的值在读取该volatile变量之后B变为可见的(来自JCIP书)。
为了保证一个动作的结果对于第二个动作是可观察到的,则第一个动作必须在第二个动作之前发生。
volatile通过防止编译器和运行时对代码进行重新排序,还限制了访问(对引用的访问)的重新排序。仅在与它们共享事前关系的操作中才能保证感知操作之间的排序约束的能力。
在底层,导致读取和写入伴随着称为内存屏障的特殊CPU指令。有关更多低级详细信息,您可以在此处和此处进行检查。 volatile
从内存可见性的角度来看,写volatile变量就像退出一个synchronized 块,读volatile变量就像进入一个synchronized块。但是请注意,这volatile 并不会阻止synchronized。
并发编程中的原子动作要么完全发生,要么根本不发生。直到动作完成,原子动作的副作用才可见。
volatile原子操作与原子操作之间存在关联:对于非volatile64位数字(长整数和双精度)原始变量,JVM可以自由地将64位读取或写入视为两个单独的32位操作。volatile在这些类型上使用可确保读取和写入操作是原子的(默认情况下,访问其余的基本 变量和引用 变量是原子的)。
volatile访问不能保证复合操作(例如增加计数器)的原子性。必须对原子变量执行复合操作,以防止数据争用和争用条件。
何时使用Volatile
volatile仅当满足以下所有条件时,我们才可以使用变量(来自JCIP书):
- 写入变量不依赖于其当前值,或者您可以确保只有一个线程更新该值。
- 该变量不与其他状态变量一起参与不变式
- 在访问变量时,由于其他任何原因都不需要锁定。
一些合适的方案是:
- 当我们有一个简单的标志(例如完成,中断或状态标志)或由多个线程访问(读取)的其他原始数据项时;
- 当我们有一个更复杂的不可变对象,该对象由一个线程初始化然后由其他线程访问时:在这里,我们要更新对不可变数据结构的引用,并允许其他线程对其进行引用。
使用Volatile之前的注意事项
- volatile与使用锁定的代码相比,依赖于变量以查看任意状态的代码更加脆弱,更难以理解。
- volatile不足以保证原子性(原子变量确实提供了原子的读-修改-写支持,通常可以用作“更好的变量”) volatile
- 同步会引入线程争用,当两个或多个线程尝试同时访问同一资源时,就会发生线程争用。
例子
1) 增加计数器
即使它显示为单个操作,“ counter ++;” 声明不是原子的。它实际上是三个不同操作的组合:读取,更新,写入。因此,即使进行计数器计数volatile也不足以避免在多线程上下文中发生数据争用:我们可能最终会丢失更新。
一些 线程安全的替代品:synchronized, 的AtomicInteger。
2) 延迟初始化的单例模式
在这种情况下,不执行INSTANCEvolatile可能会导致getInstance()返回未完全初始化的对象,从而破坏Singleton模式。为什么?在初始化对象时 ,可能会发生对参考变量INSTANCE的赋值,即使其他对象是不可变的,它也会向其他线程显示最终会改变的(错误)状态!使用volatile,我们可以保证一次性安全发布(见模式#2在这里)的对象
3) Visibility
在这种特殊 情况下,使“就绪”volatile足以使该类具有线程安全性,因为在main()更新“就绪”之后,ReaderThread将同时看到“就绪”和“编号”(发生在关系之前)的新值。但是这些构造很脆弱,我们应该避免使用它们:例如,让我们考虑有人翻转两个赋值语句的情况。
一些线程安全的替代方法:synchronized, Lock。
4) Immutable collaborator
即使(可变)Foo 包含仅引用不可变对象(Helper)的字段,Foo 也不 是线程安全的。使可变对象的引用可见时,它们可能无法完全构建。原因是,尽管共享库(Helper)是不可变的,但用于访问它的引用本身却是共享且可变的。在该示例中,这意味着一个单独的线程可以在Foo 对象的helper 字段中观察到一个过时的引用。解决这个问题的一种方法可能是成为Foo 的帮助 者; 确保在引用可见之前正确构造对象。volatilevolatile
一些线程安全的 替代方案:, AtomicReference。 synchronized
5) Compound operation
如我们在第一个示例中看到的,这里我们有一个复合操作。
volatile 在这种情况下还不够。
一些线程安全的 替代方法:synchronized, volatile+ synchronized, ReadWriteLock, AtomicBoolean。
6) 数组
这里是数组引用 ,而volatile不是数组本身。
一些线程安全 的替代品:synchronized, AtomicIntegerArray。
结论
volatile变量是一种比锁定更弱的同步形式,在某些情况下,它是锁定的一个很好的选择。如果我们遵循前面讨论的使用条件,那么与锁定相比,也许可以实现线程安全和更好的性能。但是,使用代码通常比使用锁定的代码更脆弱。 volatile volatile
volatile不仅是锁定的替代方法(在某些情况下);它也有自己的用途。
volatile 可以与其他同步机制结合使用,但是通常最好只在特定共享状态上使用一种机制。
由于volatile不会阻塞(与不同synchronized),因此没有发生死锁的可能性。但是,就像其他同步机制一样,使用volatile隐含一些性能损失。
在竞争激烈的环境中,使用volatile可能会损害性能。
请注意,volatile仅适用于字段。如果将它们全部应用于执行线程,则将其应用于方法参数或局部变量是没有意义的。
volatile 与对象引用有关,与对象本身的操作无关。
使用简单的原子变量访问比通过synchronized代码访问这些变量更有效,但是程序员需要格外小心以避免内存一致性错误。是否值得付出额外的努力取决于应用程序的大小和复杂性。
Volatile FAQs
我们可以volatile不使用而使用synchronized吗?
是的。
-我们可以volatile一起使用synchronized吗?
是的。
-我们应该volatile一起使用synchronized吗?
这取决于。
-是否volatile适用于物体?
不,它适用于对象引用 或原始类型。
-volatile解决原子性问题吗?
否,但是有一种特殊情况适用于64位基元。
-volatile和之间有synchronized什么区别?
除了状态可见性之外,synchronized关键字还通过代码块提供原子性(通过互斥)。但是该块内部的更改仅在该synchronized块退出后才对其他线程可见。
-volatile暗示线程安全吗?
完全没有:volatile,synchronized等仅是同步机制。作为开发人员,我们必须适当地使用它们,并且这些机制取决于当前的情况。
-可以volatile提高线程性能吗?
相反。使用volatile隐含一些性能损失。
链接: Java并发性:了解“Volatile”关键字_didiao java的博客-CSDN博客