Java volatile关键字:你真的懂了吗?

简介: `volatile` 是 Java 中的轻量级同步机制,主要用于保证多线程环境下共享变量的可见性和防止指令重排。它确保一个线程对 `volatile` 变量的修改能立即被其他线程看到,但不能保证原子性。典型应用场景包括状态标记、双重检查锁定和安全发布对象等。`volatile` 适用于布尔型、字节型等简单类型及引用类型,不适用于 `long` 和 `double` 类型。与 `synchronized` 不同,`volatile` 不提供互斥性,因此在需要互斥的场景下不能替代 `synchronized`。

一、volatile概念

volatile 关键字是 Java 语言中的一个轻量级的同步机制,它可以保证共享变量的可见性和有序性,但不能保证原子性。


添加图片注释,不超过 140 字(可选)


二、volatile作用

1. 可见性


添加图片注释,不超过 140 字(可选)


在 Java 中,每个线程都有自己的工作内存(缓存),用于存放它用到的变量的副本。当线程对变量进行修改时,它实际上是在自己的工作内存中对这个变量的副本进行操作,而不是直接操作主内存中的变量。只有在某个时刻,工作内存的变化才会同步到主内存中。这就导致了一个问题:当多个线程同时操作一个变量时,可能会出现变量的值在不同线程间不一致的情况。

volatile 关键字就是用来解决这个问题的。当一个变量被声明为 volatile 时,所有对这个变量的写操作都会直接同步到主内存中,所有对这个变量的读操作都会直接从主内存中读取,从而保证了变量值的一致性。

2. 防止指令重排

为了提高执行效率,编译器和处理器可能会对代码进行重排列,即调整指令的执行顺序。这可能会导致程序的执行结果与预期不符。volatile 关键字可以防止对其修饰的变量相关的操作被重排列,从而保证程序的正确性。

在底层实现上,volatile 的原理依赖于处理器提供的内存屏障(Memory Barrier)。内存屏障是一种 CPU 指令,它的作用是防止指定的内存操作被重排列。当一个变量被声明为 volatile 时,JVM 会在生成的字节码中插入内存屏障指令,以确保对这个变量的读写操作符合 volatile 的语义。


添加图片注释,不超过 140 字(可选)


可以看到

 ILOAD 1  // 将局部变量表中索引为1的int值压入操作数栈,即变量i

 ICONST_1  // 将int类型的常量1压入操作数栈

 IADD  //将栈顶的两个int值相加,并将结果压入操作数栈

 ISTORE 1  // 将栈顶的int值存入局部变量表中索引为1的位置,即变量i

可以看到一行代码变成指令后会成为多行,那么在执行多行指令CPU会存在重排的情况。


三、volatile分解

下面是一个使用volatile关键字演示可见性的Java示例,以及对其进行分析:

```java public class VolatileDemo {     // 定义一个volatile变量     private static volatile boolean flag = false;     public static void main(String[] args) {         // 创建一个线程,修改flag的值         new Thread(new Runnable() {             @Override             public void run() {                 try {                     // 等待3秒                     Thread.sleep(3000);                 } catch (InterruptedException e) {                     e.printStackTrace();                 }                 // 修改flag的值为true                 flag = true;                 System.out.println("flag已经被修改为true");             }         }).start();         // 创建另一个线程,读取flag的值         new Thread(new Runnable() {             @Override             public void run() {                 // 循环检测flag的值                 while (true) {                     //这里不增加任何代码,包括打印语句也不能增加                     if (flag) {                         // 如果flag为true,打印信息并退出循环                         System.out.println("检测到flag为true");                         break;                     }                 }             }         }).start();     } }

运行这个程序,你会看到输出如下:


添加图片注释,不超过 140 字(可选)


这说明第二个线程能够及时感知到第一个线程对flag的修改,这就是volatile变量的可见性。如果去掉volatile关键字,那么第二个线程可能会一直循环,因为它无法看到flag的变化。


添加图片注释,不超过 140 字(可选)


可以看到程序一直没有结束,在循环判断flag的值,一直没有读取到为true值。

要看到效果有个地方需要注意:


添加图片注释,不超过 140 字(可选)



四、应用场景

volatile 关键字在 Java 中是一种轻量级的同步机制,主要用于解决变量在多线程环境下的可见性问题,以及防止指令重排。以下是一些 volatile 的典型应用场景:

  1. 状态标记: 当你需要一个线程去检测某个状态的变化,并根据状态的变化做出相应的动作时,可以使用 volatile 关键字来声明这个状态变量。这样一来,当状态变量发生改变时,其他线程可以立即感知到这个变化,并作出相应的反应。

public class ShutdownHook extends Thread {     private volatile boolean shutdownRequested = false;     public void run() {         while (!shutdownRequested) {             // 业务逻辑         }     }     public void shutdown() {         shutdownRequested = true;     } }

  1. 单例模式的双重检查锁定(Double-Checked Locking):volatile 关键字可以用于实现线程安全的单例模式,其中使用双重检查锁定机制。

public class Singleton {     private static volatile Singleton instance;     public static Singleton getInstance() {         if (instance == null) {             synchronized (Singleton.class) {                 if (instance == null) {                     instance = new Singleton();                 }             }         }         return instance;     } }

  1. 安全的发布对象: 使用 volatile 关键字可以确保对象被安全的发布,即当一个线程初始化一个对象的引用并将其赋值给 volatile 变量时,其他线程可以正确地看到这个被初始化完成的对象。

public class SafePublish {     private volatile Object obj;     public void init() {         obj = new Object();     }     public Object getObj() {         return obj;     } }

这些是 volatile 关键字的一些典型应用场景,但并不局限于这些。在实际的开发中,根据具体的需求和场景,volatile 可以发挥其特性,以达到线程安全的目的。


五、相关面试题

  • volatile关键字的作用是什么?

volatile关键字的作用是保证变量的可见性和禁止指令重排序。当一个变量被声明为 volatile,任何对它的写操作都将立即反映到其他线程中,以确保多线程环境下的数据一致性。

  • volatile关键字能保证哪些特性?

volatile关键字能保证变量的可见性,即一个线程修改了共享变量,其他线程可以立即看到修改后的值。volatile关键字还能禁止指令重排序,确保程序执行的正确性。

  • volatile关键字不能保证哪些特性?

volatile关键字不能保证原子性,因此不能用于需要原子操作的场景。volatile关键字也不能保证顺序性,因此不能用于需要顺序执行的场景。

  • volatile关键字的底层实现原理是什么?

volatile关键字的底层实现原理是通过内存屏障来实现的。内存屏障是一种CPU指令,它的作用是禁止指令重排序,保证特定操作的执行顺序。

  • volatile与synchronized的区别?

volatile和synchronized都是Java语言提供的同步机制,但它们之间存在一些重要区别。

特性

volatile

synchronized

作用域

变量

变量、方法、对象

可见性

保证

保证

原子性

不保证

保证

适用场景

保证共享变量的可见性和禁止指令重排序

保证共享变量的原子性和可见性

volatile 和 synchronized 都用于确保多线程环境下的数据一致性,但它们有几个重要的区别。volatile 用于修饰变量,保证可见性,但不提供互斥性。synchronized 用于修饰代码块或方法,提供互斥性,同时也保证可见性。另外,synchronized 会引入性能开销,而 volatile 不会。

  • 什么是可见性问题,volatile 如何解决它?

可见性问题是指一个线程对共享变量的修改可能不被其他线程立即感知。volatile 通过强制线程从主内存中读取变量的值,而不是从线程的本地缓存中,来解决可见性问题。这确保了一个线程对 volatile 变量的修改对其他线程是可见的。

  • 什么情况下应该使用 volatile?

volatile 主要用于标记变量,当变量被多个线程共享并且一个线程修改了这个变量的值后需要立即通知其他线程,以确保可见性和避免指令重排序时,应该使用 volatile。典型的使用场景包括标记线程是否结束、双重检查锁定等。

  • volatile 能够替代 synchronized 吗?

不完全可以替代。volatile 主要用于确保变量的可见性,而 synchronized 用于确保互斥性。如果需要保证多线程下的互斥性,volatile 无法满足需求,需要使用 synchronized 或其他锁机制。通常,volatile 用于一些特定的场景,而 synchronized 用于更复杂的同步需求。

  • volatile 适用于什么类型的变量?

volatile 适用于布尔型、字节型、短整型、字符型、引用类型等变量。它通常不适用于 long 和 double 类型,因为它们在 Java 中不是原子操作,可能会引发一些线程安全问题。

  • 什么是指令重排序,volatile 如何避免它?

指令重排序是编译器或处理器为了提高性能而重新排序执行指令的顺序,但有时可能导致不正确的结果。volatile 可以避免指令重排序,因为它禁止编译器和处理器对标记为 volatile 的变量的读写指令进行重排序。

目录
相关文章
|
4天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
4天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
1月前
|
JavaScript 前端开发 Java
java中的this关键字
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。自学前端2年半,正向全栈进发。若我的文章对你有帮助,欢迎关注,持续更新中!🎉🎉🎉
52 9
|
1月前
|
设计模式 JavaScript 前端开发
java中的static关键字
欢迎来到瑞雨溪的博客,博主是一名热爱JavaScript和Vue的大一学生,致力于全栈开发。如果你从我的文章中受益,欢迎关注我,将持续分享更多优质内容。你的支持是我前进的动力!🎉🎉🎉
51 8
|
1月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
47 4
|
2月前
|
Java 程序员
在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。
【10月更文挑战第13天】在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。本文介绍了Java关键字的基本概念及其重要性,并通过定义类和对象、控制流程、访问修饰符等示例,展示了关键字的实际应用。掌握这些关键字,是成为优秀Java程序员的基础。
32 3
|
2月前
|
算法 Java
在Java编程中,关键字和保留字是基础且重要的组成部分,正确理解和使用它们
【10月更文挑战第13天】在Java编程中,关键字和保留字是基础且重要的组成部分。正确理解和使用它们,如class、int、for、while等,不仅能够避免语法错误,还能提升代码的可读性和执行效率。本指南将通过解答常见问题,帮助你掌握Java关键字的正确使用方法,以及如何避免误用保留字,使你的代码更加高效流畅。
42 3
|
2月前
|
存储 安全 Java
了解final关键字在Java并发编程领域的作用吗?
在Java并发编程中,`final`关键字不仅用于修饰变量、方法和类,还在多线程环境中确保对象状态的可见性和不变性。本文深入探讨了`final`关键字的作用,特别是其在final域重排序规则中的应用,以及如何防止对象的“部分创建”问题,确保线程安全。通过具体示例,文章详细解析了final域的写入和读取操作的重排序规则,以及这些规则在不同处理器上的实现差异。
了解final关键字在Java并发编程领域的作用吗?
|
2月前
|
Java 编译器
在Java中,关于final、static关键字与方法的重写和继承【易错点】
在Java中,关于final、static关键字与方法的重写和继承【易错点】
31 5
|
2月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
31 2