问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

volatile关键字详解,看了包会!

创作时间:
作者:
@小白创作中心

volatile关键字详解,看了包会!

引用
CSDN
1.
https://m.blog.csdn.net/qq_56158663/article/details/145806923

一、 volatile 是什么?

你可以把 volatile 想象成一个“小喇叭” 📢,专门用来修饰 Java 中的变量。这个“小喇叭”的作用是:

  • 保证变量的“新鲜度”:当一个变量被 volatile 修饰后,任何线程对这个变量的修改,都会立刻“广播”给所有其他线程。也就是说,其他线程会立刻知道这个变量的值变了,而不是用自己缓存的旧值。 🤩
  • 禁止指令重排序:编译器为了优化代码,可能会调整代码的执行顺序(指令重排序)。volatile 可以阻止这种优化,保证代码按照你写的顺序执行。 🚫

二、 volatile 解决了什么问题?

在多线程环境下,每个线程都有自己的“小仓库”(工作内存),用来存放共享变量的副本。如果没有 volatile,可能会出现以下问题:

  • 数据不一致:线程 A 修改了变量的值,但线程 B 可能还在用自己“小仓库”里的旧值,导致数据不一致。 😫
  • 程序出错:某些操作依赖于变量的实时状态,如果线程拿到的不是最新的值,程序可能会出错。 🤕

volatile 就是为了解决这些问题而生的。它可以确保所有线程都能看到共享变量的最新值,避免数据不一致和程序出错。 😎

举个例子:

想象一下,你和你的朋友们一起玩一个猜数字游戏。

  • **没有 volatile:**你写下了一个数字,然后告诉你的朋友们。但是,你的朋友们可能没有立刻听到你的话,他们还在用自己之前猜的数字。这样,游戏就很难进行下去,因为大家用的数字不一样。 😕
  • **有 volatile:**你写下了一个数字,然后用一个“大喇叭” 📢 告诉你的朋友们。你的朋友们立刻就能听到你的话,知道你写下的数字是什么。这样,大家用的数字就是一样的,游戏就能顺利进行下去。 😄

三、 怎么使用 volatile?

使用 volatile 很简单,只需要在变量声明的时候加上 volatile 关键字即可。

示例代码:

public class VolatileExample {
    // 使用 volatile 修饰的变量
    private volatile boolean running = true;
    public void start() {
        System.out.println("线程开始运行...");
        while (running) {
            // 执行一些操作
        }
        System.out.println("线程停止运行...");
    }
    public void stop() {
        running = false; // 修改 running 的值
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileExample example = new VolatileExample();
        // 启动一个线程
        Thread thread = new Thread(example::start);
        thread.start();
        // 等待一段时间
        Thread.sleep(1000);
        // 停止线程
        example.stop();
    }
}

代码解释:

  1. running 变量被 volatile 修饰,表示它是共享的,并且需要保证可见性。
  2. start() 方法在一个循环中执行一些操作,直到 running 变为 false
  3. stop() 方法将 running 设置为 false,通知线程停止运行。
  4. main() 方法中,我们启动一个线程,然后等待一段时间,最后调用 stop() 方法停止线程。

如果没有 volatile

线程可能永远不会停止,因为它可能一直在使用自己缓存的 running 值,而不知道 running 已经被修改为 false 了。 😵
有了 volatile

stop() 方法将 running 设置为 false 时,这个修改会立刻被“广播”给所有线程,包括正在运行的线程。线程会立刻知道 running 变成了 false,然后停止运行。 🥳

四、 volatile 的局限性(重要!)

volatile 只能保证变量的可见性,不能保证原子性。 ⚠️

原子性:一个操作要么全部完成,要么完全不完成,不会被其他线程中断。

举个例子:

volatile int count = 0;
public void increment() {
    count++; // 这不是一个原子操作
}

count++ 实际上包含了三个操作:

  1. 读取 count 的值。
  2. count 的值加 1。
  3. 将新的值写回 count

如果多个线程同时执行 increment() 方法,可能会出现以下情况:

  1. 线程 A 读取 count 的值为 0。
  2. 线程 B 读取 count 的值为 0。
  3. 线程 A 将 count 的值加 1,然后写回 countcount 的值为 1。
  4. 线程 B 将 count 的值加 1,然后写回 countcount 的值为 1。(而不是期望的 2) 🤦‍♀️

这就是因为 count++ 不是一个原子操作,多个线程同时执行时可能会互相干扰。

总结:

  • volatile 保证可见性,但不保证原子性。
  • 如果需要保证原子性,可以使用 synchronized 关键字或者 java.util.concurrent 包中的原子类(如 AtomicInteger)。 👍

五、 什么时候使用 volatile?

  • 当一个变量被多个线程共享,并且一个线程修改了变量的值,其他线程需要立刻知道这个修改。
  • 当需要禁止指令重排序,保证代码按照你写的顺序执行。

六、 volatile 在单例模式中的应用(双重检查锁)

想象一下,你要创建一个“独一无二”的对象,就像一个班级里只有一个班长。这个“独一无二”的对象就是单例模式要实现的目标。

双重检查锁(Double-Checked Locking)就像一个“谨慎”的班长选举方法。它想尽量减少大家排队投票的时间,所以设计了一个“两次检查”的机制。

代码示例:

public class Singleton {
    private volatile static Singleton instance; // 使用 volatile,很重要!
    private Singleton() {
        // 私有构造方法,防止别人自己选班长
    }
    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查:看看有没有班长了?
            synchronized (Singleton.class) { // 只有没班长的时候,才需要排队选班长
                if (instance == null) { // 第二次检查:排队的时候,可能别人已经选了班长,再确认一下
                    instance = new Singleton(); // 终于选出班长了!
                }
            }
        }
        return instance; // 返回班长
    }
}

问题:如果没有 volatile,选班长可能会出什么问题?

instance = new Singleton(); 这行代码,看起来很简单,但实际上计算机要做好几件事:

  1. 找个空教室:分配一块内存空间给新的班长(Singleton 对象)。
  2. 给班长发教材:初始化班长(Singleton 对象)。
  3. 贴个公告:instance 指向这个新班长。

指令重排序捣乱:

如果没有 volatile,计算机可能会偷懒,先“贴个公告”,再“给班长发教材”。 也就是说,先让 instance 指向了空教室,但教室里还没人,教材也没发。

线程安全问题:

  • 线程 A:正在“选班长”,已经“贴了公告”(instance 不为 null 了),但还没“发教材”。
  • 线程 B:跑过来一看,“公告”上说已经有班长了(instance 不为 null),就直接去“找班长”了。
  • 结果:线程 B 找到的是一个“空教室”,啥也没有,用起来肯定会出问题! 😱

volatile 的作用:

volatile 就像一个“强制规定”,告诉计算机必须先“发教材”,再“贴公告”。 这样,即使线程 A 还没“发完教材”,线程 B 也不会看到“公告”,就不会拿到一个“空教室”了。 😎

总结:

  • 双重检查锁就像一个“谨慎”的班长选举方法,想减少排队时间。
  • 如果没有 volatile,计算机可能会偷懒,导致线程拿到一个“空教室”(未初始化的对象)。
  • volatile 就像一个“强制规定”,保证计算机按照正确的顺序“选班长”,避免出现问题。

更简洁的单例模式(推荐):

虽然双重检查锁是一种常见的单例模式实现方式,但它比较复杂,容易出错。更推荐使用静态内部类的方式来实现单例模式,这种方式更加简洁、安全,而且不需要使用 volatile 关键字。 👍

public class Singleton {
    private Singleton() {
        // 私有构造方法,防止外部实例化
    }
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

解释:

  • SingletonHolder 是一个静态内部类,只有在调用 getInstance() 方法时才会被加载。
  • INSTANCE 是一个静态常量,在类加载时会被初始化,而且只会被初始化一次。
  • 由于类加载是线程安全的,因此这种方式可以保证单例的线程安全。

七、总结

volatile 是一个轻量级的同步机制,可以保证变量的可见性,但不能保证原子性。在多线程编程中,需要根据具体情况选择合适的同步机制。在双重检查锁的单例模式中,volatile 可以防止指令重排序,确保线程安全。但更推荐使用静态内部类的方式来实现单例模式,这种方式更加简洁、安全,而且不需要使用 volatile 关键字。 🤔

希望这篇文章能够帮助你理解 volatile 关键字! 记住,理解概念最重要,然后才能灵活运用。 🧠 加油! 💪

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号