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

发布于:2023-10-25 ⋅ 阅读:(21) ⋅ 点赞:(0)

一、volatile概念

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

二、volatile作用

1. 可见性

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

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

2. 防止指令重排

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

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

可以看到

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();
    }
}

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

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

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

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

四、应用场景

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 的变量的读写指令进行重排序。