Java 中的 Volatile 是如何工作的?
什么是Java中的 volatile 变量以及什么时候使用Java中的 volatile 变量是Java面试中著名的多线程面试题? 尽管许多程序员知道什么是 volatile 变量,但他们在第二部分失败了,即在 Java 中使用 volatile 变量,因为在 Java 中对 volatile 变量有清晰的理解和实践并不常见。
在本篇文章中,我们将通过提供 Java 中 volatile 变量的简单示例并讨论何时在 Java 中使用 volatile 变量来弥补这一差距。 无论如何,Java 中的 volatile 关键字用作指示 Java 编译器和 Thread 不缓存此变量的值并始终从主内存中读取它。
因此,如果大家想通过在 int
或 boolean
变量中读取和写入等实现来共享读写操作是原子的任何变量,那么我们可以将它们声明为 volatile
变量。
从 Java 5 开始,随着自动装箱、枚举、泛型和变量参数等重大变化,Java 在 Java 内存模型 (JMM) 中引入了一些变化,这保证了从一个线程到另一个线程所做的更改的可见性,也作为“先于发生” 解决了发生在一个线程中的内存写入可以“泄漏”并被另一个线程看到的问题。
Java volatile 关键字不能与方法或类一起使用,它只能与变量一起使用。 Java volatile 关键字还保证可见性和顺序,在 Java 5 写入任何 volatile 变量之后发生在任何读入 volatile 变量之前。
顺便说一句,使用 volatile 关键字还可以防止编译器或 JVM 对代码进行重新排序或将它们从同步屏障中移开。
Java 中的 Volatile 变量示例
为了理解 java 中 volatile 关键字的例子,让我们回到 Java 中的单例模式,看看在单例中使用 Volatile 和 java 中没有 volatile 关键字的双重检查锁定。
/**
* 用于演示在 Java 中何处使用 Volatile 关键字的 Java 程序。
* 在此示例中,Singleton Instance 被声明为 volatile 变量,
* 以确保每个线程都能看到 _instance 的更新值。
*
* @author Zadmei
*/
public class Singleton{
private static volatile Singleton _instance; //volatile variable
public static Singleton getInstance(){
if(_instance == null){
synchronized(Singleton.class){
if(_instance == null)
_instance = new Singleton();
}
}
return _instance;
}
如果仔细查看代码,我们将能够弄清楚:
- 我们只创建一次实例
- 我们在第一个请求到来时懒惰地创建实例。
如果我们不让 _instance
变量为 volatile,那么正在创建 Singleton
实例的 Thread 将无法与其他线程通信,该实例已经创建,直到它从 Singleton
块中出来,所以如果 Thread A 正在创建 Singleton
实例并且 就在创建失去 CPU 之后,所有其他线程将无法将 _instance
的值视为不为空,并且它们会认为它仍然为空。
为什么? 因为读取线程没有进行任何锁定,并且在写入线程退出同步块之前,内存不会同步,并且 _instance
的值不会在主内存中更新。
使用 Java 中的 Volatile 关键字,这由 Java 自己处理,并且所有读取器线程都可以看到此类更新。 所以在总结中除了Java中的synchronized
关键字外,还使用了一个 volatile 关键字来实现线程间内存内容的通信。
让我们看一下 Java 中 volatile 关键字的另一个例子
大多数时候在编写游戏时我们使用变量 bExit 来检查用户是否按下了退出按钮,这个变量的值在事件线程中更新并在游戏线程中检查,所以如果我们不使用 带有此变量的 volatile 关键字,如果 Game Thread 尚未在 Java 中同步,它可能会错过来自事件处理程序线程的更新。
java中的 volatile 关键字保证了 volatile 变量的值总是从主存中读取,Java内存模型中的“happens-before”关系保证了内存中的内容会被传递给不同的线程。
private boolean bExit;
while(!bExit) {
checkUserPosition();
updateUserPosition();
}
在此代码示例中,一个线程(游戏线程)可以缓存“bExit”的值,而不是每次都从主内存中获取它,如果在任何其他线程(事件处理程序线程)之间更改该值; 该线程将看不到它。 在 Java 中将布尔变量“bExit”设置为 volatile 可确保不会发生这种情况。
什么时候在 Java 中使用 Volatile 变量?
学习 volatile 关键字最重要的事情之一是了解何时在 Java 中使用 volatile 变量。 许多程序员知道什么是 volatile 变量以及它是如何工作的,但他们从未真正将 volatile 修饰符用于任何实际目的。 下面是几个示例来演示何时在 Java 中使用 volatile 关键字:
1. 如果你想原子地读写long
和double
变量,你可以使用 Volatile 变量。 long
和 double
都是 64 位数据类型,默认情况下 long
和 double
的写入不依赖于原子和平台。
许多平台在 long
和 double
变量 2 步骤中执行写入,在每个步骤中写入 32 位,因此线程可能会看到来自两个不同写入器的 32 位。 我们可以通过在 Java 中将 long
和 double
变量设置为 volatile 来避免这个问题。
2. 在某些情况下,volatile 变量可以用作在 Java 中实现同步的替代方法,例如 Visibility
。 使用 volatile 变量,可以保证一旦写入操作完成,所有读取线程都会看到 volatile 变量的更新值,如果没有 volatile 关键字,不同的读取线程可能会看到不同的值。
3. volatile 变量可用于通知编译器某个特定字段可能会被多个线程访问,这将阻止编译器进行任何重新排序或任何类型的优化,这在多线程环境中是不可取的。
如果没有 volatile 变量,编译器可以重新排序代码,自由缓存 volatile 变量的值,而不是总是从主内存中读取。 像下面没有 volatile 变量的例子可能会导致无限循环
private boolean isActive = thread;
public void printMessage(){
while(isActive){
System.out.println("Thread is Active");
}
}
如果没有 volatile 修饰符,则不能保证一个线程从其他线程看到 isActive
的更新值。 编译器也可以自由缓存 isActive
的值,而不是在每次迭代时从主内存中读取它。 通过使 isActive
成为 volatile 变量,我们可以避免这些问题。
4. 另一个可以使用 volatile 变量的地方是修复单例模式中的双重检查锁定。 正如我们在 Why should you use Enum as Singleton 中讨论的那样,双重检查锁定在 Java 1.4 环境中被破坏了?
Java 中 Volatile 关键字的要点
- Java中的 volatile 关键字是唯一对变量的应用,在类和方法中使用 volatile 关键字是非法的。
- Java中的 volatile 关键字保证 volatile 变量的值总是从主存中读取,而不是从Thread的本地缓存中读取。
- 在 Java 中,对于所有使用 Java volatile 关键字声明的变量(包括
long
和double
变量),读写都是原子的。 - 在 Java 中对变量使用 volatile 关键字可以降低内存一致性错误的风险,因为在 Java 中对 volatile 变量的任何写入都会与对该相同变量的后续读取建立先行关系。
- 从 Java 5 开始,对 volatile 变量的更改始终对其他线程可见。 更重要的是,这也意味着当线程读取 Java 中的 volatile 变量时,它不仅会看到对 volatile 变量的最新更改,还会看到导致更改的代码的副作用。
- 即使在 Java 中没有使用 volatile 关键字,对于大多数原始变量(除
long
和double
之外的所有类型),引用变量的读写都是原子的。 - 访问 Java 中的 volatile 变量永远不会有阻塞的机会,因为我们只是在进行简单的读取或写入,所以与同步块不同,我们永远不会持有任何锁或等待任何锁。
- 作为对象引用的 Java volatile 变量可能为
null
。 - Java volatile 关键字并不意味着原子,这是一个常见的误解,认为在声明
volatile ++
将是原子的之后,要使操作成为原子,我们仍然需要使用 Java 中的同步方法或块来确保独占访问。 - 如果一个变量不在多个线程之间共享,则不需要对该变量使用 volatile 关键字。
Java 中 synchronized 和 volatile 关键字的区别
volatile 和 synchronized 之间的区别是多线程和并发面试中另一个流行的核心 Java 问题。 请记住,volatile 不是同步关键字的替代品,但在某些情况下可以用作替代品。
以下是 Java 中 volatile 和 synchronized 关键字之间的一些区别。
- Java中的 volatile 关键字是字段修饰符,synchronized 修饰的是代码块和方法。
- synchronized 获取和释放
monitor
的锁,Java的 volatile 关键字不需要。 - Java中的线程在同步的情况下可以阻塞等待任何监视器,而Java中的 volatile 关键字则不会。
- synchronized 方法比 Java 中的 volatile 关键字更能影响性能。
- 由于Java中的 volatile 关键字只同步线程内存和“主”内存之间的一个变量的值,而synchronized同步线程内存和“主”内存之间的所有变量的值并锁定和释放监视器以启动。 由于这个原因,Java 中的 synchronized 关键字可能比 volatile 有更多的开销。
- 我们不能在空对象上进行同步,但 Java 中的 volatile 变量可能为空。
- 从 Java 5 开始,写入 volatile 字段与监视器释放具有相同的记忆效应,从 volatile 字段读取与监视器获取具有相同的记忆效应
简而言之,Java 中的 volatile 关键字不是同步块或方法的替代品,但在某些情况下非常方便,并且可以节省 Java 中使用同步带来的性能开销。