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

volatile关键字详解,看了包会!_第1张图片

目录

    • 一、 volatile 是什么?
    • 二、 volatile 解决了什么问题?
    • 三、 怎么使用 volatile?
    • 四、 volatile 的局限性(重要!)
    • 五、 什么时候使用 volatile?
    • 六、 volatile 在单例模式中的应用(双重检查锁)
    • 七、总结

我的其他文章也讲解的比较有趣,如果喜欢博主的讲解方式,可以多多支持一下,感谢!
了解synchronized关键字请看: synchronized 关键字:线程同步的“VIP 包间”

今儿个就让我们来讲解一下Java中的volatile关键字

一、 volatile 是什么?

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

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

二、 volatile 解决了什么问题?

volatile关键字详解,看了包会!_第2张图片

在多线程环境下,每个线程都有自己的“小仓库”(工作内存),用来存放共享变量的副本。如果没有 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 关键字! 记住,理解概念最重要,然后才能灵活运用。 加油!

你可能感兴趣的:(多线程,java,开发语言)