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 Jiyik
*/
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 中使用同步带来的性能开销。
相关文章
在 Java 中获取文件大小
发布时间:2023/05/01 浏览次数:139 分类:Java
-
Java 提供了不同的方法来获取文件的字节大小。 本教程演示了在 Java 中获取文件大小的不同方法。使用 Java IO 的文件类获取文件大小 Java IO 包的 File 类提供了以字节为单位获取文件大小的功能。
Java 中的文件分隔符
发布时间:2023/05/01 浏览次数:108 分类:Java
-
本篇文章介绍了 Java 中的文件分隔符。Java 中的文件分隔符 文件分隔符是用来分隔目录的字符; 例如,Unix 使用 /,Windows 使用 \ 作为文件分隔符。
Java 中的文件过滤器
发布时间:2023/05/01 浏览次数:193 分类:Java
-
本篇文章介绍如何在 Java 中使用 FileFilter。FileFilter 用于过滤具有特定扩展名的文件。 Java内置包IO和Apache Commons IO为FileFilter提供了类和接口来进行文件过滤操作。
Java 获取 ISO 8601 格式的当前时间戳
发布时间:2023/05/01 浏览次数:132 分类:Java
-
本篇文章介绍了 ISO 8601 日期格式、其重要性及其在 Java 中的使用。 它还列出了一些优点来强调为什么应该使用 ISO 格式来表示日期。
在 Java 中获取数组的子集
发布时间:2023/05/01 浏览次数:142 分类:Java
-
本篇文章介绍了几种在 Java 中获取数组子集的方法。使用 Arrays.copyOf() 方法获取数组的子集 使用 Arrays.copyOfRange() 方法获取数组的子集
用 Java 填充二维数组
发布时间:2023/05/01 浏览次数:110 分类:Java
-
二维数组是基于表结构的,即行和列,填充二维数组不能通过简单的添加到数组操作来完成。 本篇文章介绍如何在 Java 中填充二维数组。
计算 Java 数组中的重复元素
发布时间:2023/05/01 浏览次数:202 分类:Java
-
本篇文章介绍Java计算数组中重复元素的方法。计算 Java 数组中的重复元素。我们可以创建一个程序来计算数组中的重复元素。 该数组可以是未排序的,也可以是已排序的。