Java中最简单易懂的易失性关键字示例

2022-08-31 12:07:50

我正在阅读Java中的易失性关键字,并完全理解其中的理论部分。

但是,我正在寻找的是一个很好的例子,它显示了如果变量不是易失性的,如果它是可变的,会发生什么。

下面的代码片段无法按预期工作(取自此处):

class Test extends Thread {

    boolean keepRunning = true;

    public void run() {
        while (keepRunning) {
        }

        System.out.println("Thread terminated.");
    }

    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        t.start();
        Thread.sleep(1000);
        t.keepRunning = false;
        System.out.println("keepRunning set to false.");
    }
}

理想情况下,如果不是易失性的,线程应该无限期地保持运行。但是,它确实会在几秒钟后停止。keepRunning

我有两个基本问题:

  • 任何人都可以用例子来解释挥发性吗?不是JLS的理论。
  • 易失性是否替代同步?它实现了原子性吗?

答案 1

易失性 --> 保证可见性而不是原子性

同步(锁定) --> 保证可见性和原子性(如果操作正确)

易失性不能替代同步

仅当更新引用而不对其执行某些其他操作时,才使用易失性。

例:

volatile int i = 0;

public void incrementI(){
   i++;
}

如果不使用同步或 AtomicInteger,则不会是线程安全的,因为递增是一种复合操作。

为什么程序不能无限期运行?

好吧,这取决于各种情况。在大多数情况下,JVM 足够智能,可以刷新内容。

挥发性的正确使用讨论了挥发性的各种可能用途。正确使用易失性是很棘手的,我会说“当有疑问时,把它排除在外”,使用同步块代替。

也:

同步块可用于代替易失性,但反之则不然


答案 2

对于您的特定示例:如果未声明易失性,服务器 JVM 可以将变量从循环中提升出来,因为它不会在循环中修改(将其转换为无限循环),但客户端 JVM 不会。这就是为什么你会看到不同的结果。keepRunning

关于易失性变量的一般解释如下:

当一个字段被声明时,编译器和运行时会注意到这个变量是共享的,并且对它的操作不应该与其他内存操作重新排序。易失性变量不会缓存在寄存器或缓存中,因为它们对其他处理器隐藏,因此读取易失性变量始终返回任何线程的最新写入volatile

可变变量的可见性效应超出了可变变量本身的值。当线程 A 写入易失性变量,随后线程 B 读取同一变量时,在写入易失性变量之前对 A 可见的所有变量的值在读取易失性变量后对 B 可见。

可变变量最常见的用途是用作完成、中断或状态标志:

  volatile boolean flag;
  while (!flag)  {
     // do something untill flag is true
  }

可变变量可用于其他类型的状态信息,但在尝试此操作时需要更加小心。例如,volatile 的语义不够强大,无法使增量运算 () 成为原子,除非您可以保证变量仅从单个线程写入。count++

锁定可以保证可见性和原子性;可变变量只能保证可见性。

仅当满足以下所有条件时,才能使用可变变量:

  • 写入变量不依赖于其当前值,或者您可以确保只有单个线程更新该值;
  • 该变量不与其他状态变量参与不变量;和
  • 在访问变量时,由于任何其他原因,不需要锁定。

调试提示:在调用JVM时,一定要始终指定JVM命令行切换,即使是为了开发和测试。服务器 JVM 比客户端 JVM 执行更多的优化,例如将未在循环中修改的变量从循环中提升出来;在开发环境(客户端 JVM)中可能看起来有效的代码可能会在部署环境(服务器 JVM)中中断。-server

这是“Java并发实践”的摘录,这是你能找到的关于这个主题的最好的书。


推荐