1. 概述

在并发编程中,多个线程访问共享变量时,如果不加控制,可能会出现可见性问题。Kotlin 提供了 @Volatile 注解,用于确保变量的修改对其他线程立即可见。

本文将介绍:

  • Kotlin 中 @Volatile 注解的使用
  • 其底层在 JVM 字节码中的表示
  • 为什么需要它以及它解决了什么问题

2. Volatile 属性详解

如果你有 Java 背景,你可能知道 Java 中的 volatile 关键字。那么在 Kotlin 中,对应的机制是什么呢?

来看一个实际的例子:

object TaskExecutor : Runnable {

    private var shouldContinue = true

    override fun run() {
        while (shouldContinue) {}
        println("Done")
    }

    fun stop() {
        shouldContinue = false
    }
}

这是一个单例对象,它会在一个线程中运行一个忙等待循环,直到 shouldContinue 变为 false

我们期望在调用 stop() 方法后,线程能立刻退出循环并打印 "Done"。但实际情况可能并非如此 ⚠️

为什么会出现问题?

由于现代 CPU 架构存在缓存一致性问题,线程可能读取到的是变量的本地副本(缓存),而不是主内存中的最新值。这就可能导致线程永远读不到 shouldContinue 的最新状态。

解决方案:使用 @Volatile

只需给变量加上 @Volatile 注解即可解决这个问题:

object TaskExecutor : Runnable {

    @Volatile
    private var shouldContinue = true

    // 其余代码不变
}

@Volatile 的作用是告诉 JVM:这个变量的写操作必须立即刷新到主内存,读操作必须从主内存读取最新值。

换句话说,它保证了变量的 可见性,但不保证原子性。例如 i++ 这样的操作仍然不是线程安全的。

3. JVM 字节码层面的表示

我们来看看 @Volatile 在字节码中是如何表示的。

kotlinc 编译上面的 Kotlin 文件:

$ kotlinc TaskExecutor.kt

然后使用 javap 查看生成的字节码:

$ javap -v -p -c TaskExecutor

输出中会看到如下内容(简化):

private static volatile boolean shouldContinue;
    descriptor: Z
    flags: (0x004a) ACC_PRIVATE, ACC_STATIC, ACC_VOLATILE

可以看到,shouldContinue 被标记为 ACC_VOLATILE,也就是 Java 字节码层面对 volatile 的表示。

⚠️ 这说明 Kotlin 的 @Volatile 实际上就是 Java 中 volatile 的等价物。

4. 小结

本文我们介绍了 Kotlin 中的 @Volatile 注解及其作用:

  • 用于确保变量在多线程间的可见性
  • 不保证操作的原子性
  • 底层通过在 JVM 字节码中标记 ACC_VOLATILE 实现

如果你对 volatile 更深入的语义感兴趣,比如内存屏障、happens-before 规则等,推荐阅读我们关于 Java volatile 的详细指南

完整示例代码可在 GitHub 上获取。


原始标题:Volatile Properties in Kotlin