ChatGPT解决这个技术问题 Extra ChatGPT

volatile 关键字有什么用?

今天在工作中,我遇到了 Java 中的 volatile 关键字。由于不是很熟悉,我找到了 this explanation

鉴于那篇文章解释了相关关键字的详细信息,您是否曾经使用过它,或者您是否曾经看到过可以以正确方式使用该关键字的案例?


B
Basil Bourque

volatile 具有内存可见性的语义。基本上,在完成写入操作后,volatile 字段的值对所有读取器(特别是其他线程)可见。如果没有 volatile,读者可能会看到一些未更新的值。

回答您的问题:是的,我使用 volatile 变量来控制某些代码是否继续循环。循环测试 volatile 值,如果是 true 则继续。可以通过调用“停止”方法将条件设置为 false。循环看到 false 并在 stop 方法完成执行后测试该值时终止。

我强烈推荐的《Java Concurrency in Practice》一书很好地解释了volatile。这本书是由撰写问题中引用的 IBM 文章的同一个人撰写的(事实上,他在该文章的底部引用了他的书)。我对 volatile 的使用就是他的文章所说的“模式 1 状态标志”。

如果您想详细了解 volatile 如何在后台工作,请阅读 the Java memory model。如果您想超越该级别,请查看像 Hennessy & Patterson 这样的优秀计算机体系结构书籍,并阅读有关缓存一致性和缓存一致性的内容。


这个答案是正确的,但不完整。它省略了 JSR 133 中定义的新 Java 内存模型附带的 volatile 的一个重要属性:当线程读取 volatile 变量时,它不仅会看到某个其他线程最后写入它的值,而且还会看到所有其他写入在 volatile 写入时在该其他线程中可见的其他变量。请参阅 this answerthis reference
对于初学者,我会要求你用一些代码来演示(好吗?)
问题中链接的文章有代码示例。
我认为“Hennessy & Patterson”链接已损坏。与“Java 内存模型”的链接实际上指向 Oracle 的 Java 语言规范“第 17 章。线程和锁”。
@fefrei:“立即”是一个口语术语。当然,当实际指定执行时间和线程调度算法时,不能保证这一点。程序确定易失性读取是否在特定易失性写入之后的唯一方法是检查所看到的值是否是预期的写入值。
9
9 revs, 2 users 92%

“... volatile 修饰符保证读取字段的任何线程都会看到最近写入的值。” - Josh Bloch

如果您正在考虑使用 volatile,阅读处理原子行为的包 java.util.concurrent

Singleton Pattern 上的 Wikipedia 帖子显示使用中的 volatile。


为什么同时存在 volatilesynchronized 关键字?
此后,关于单例模式的 Wikipedia 文章发生了很大变化,并且不再包含上述 volatile 示例。它可以在 in an archived version 中找到。
@ptkato 这两个关键字的用途完全不同,所以这个问题作为比较没有多大意义,尽管它们都与并发有关。这就像在说“为什么同时存在 voidpublic 关键字”。
所以...简而言之,volatile 与类上的 static 有点相似?其中一个类的多个实例可以共享相同的变量/属性。
@Aruman我不认为这是正确的。 static 变量可能必须是 volatile,以防有多个线程读取它,这两者是不相关的。
M
Marco Luzzara

挥发性(vɒlətʌɪl):在常温下容易蒸发

关于 volatile 的要点:

Java 中的同步可以通过使用 Java 关键字 synchronized 和 volatile 和 locks 来实现。在Java中,我们不能有同步变量。对变量使用 synchronized 关键字是非法的,会导致编译错误。在 Java 中不使用同步变量,您可以使用 java volatile 变量,它会指示 JVM 线程从主内存中读取 volatile 变量的值,而不是在本地缓存它。如果一个变量没有在多个线程之间共享,则不需要使用 volatile 关键字。

source

volatile 的用法示例:

public class Singleton {
    private static volatile Singleton _instance; // volatile variable
    public static Singleton getInstance() {
        if (_instance == null) {
            synchronized (Singleton.class) {
                if (_instance == null)
                    _instance = new Singleton();
            }
        }
        return _instance;
    }
}

我们在第一个请求到来时懒惰地创建实例。

如果我们不创建 _instance 变量 volatile,则创建 Singleton 实例的线程无法与其他线程通信。因此,如果线程 A 正在创建 Singleton 实例,并且在创建之后,CPU 损坏等,所有其他线程将无法看到 _instance 的值不为空,他们会认为它仍然被分配为空。

为什么会这样?因为读取线程没有进行任何锁定,并且直到写入线程退出同步块,内存将不会同步并且 _instance 的值不会在主内存中更新。使用 Java 中的 Volatile 关键字,这是由 Java 本身处理的,并且所有读取器线程都可以看到此类更新。

结论:volatile关键字也被用来在线程之间传递内存的内容。

不带 volatile 的示例用法:

public class Singleton {    
    private static Singleton _instance;   //without volatile variable
    public static Singleton getInstance() {   
        if (_instance == null) {  
            synchronized(Singleton.class) {  
                if (_instance == null) 
                    _instance = new Singleton(); 
            } 
        }
        return _instance;  
    }
}

上面的代码不是线程安全的。尽管它在同步块中再次检查实例的值(出于性能原因),但 JIT 编译器可以重新排列字节码,以便在构造函数完成执行之前设置对实例的引用。这意味着方法 getInstance() 返回一个可能尚未完全初始化的对象。为了使代码线程安全,从 Java 5 开始可以使用关键字 volatile 作为实例变量。标记为 volatile 的变量只有在对象的构造函数完全完成执行后才对其他线程可见。
Source

https://i.stack.imgur.com/bgHrU.png

volatile Java 中的用法

快速失败迭代器通常使用列表对象上的 volatile 计数器实现。

当列表更新时,计数器增加。

创建迭代器时,计数器的当前值嵌入到迭代器对象中。

执行迭代器操作时,该方法会比较两个计数器值,如果它们不同则抛出 ConcurrentModificationException。

故障安全迭代器的实现通常是轻量级的。它们通常依赖于特定列表实现的数据结构的属性。没有通用模式。


“快速失败的迭代器通常使用 volatile 计数器实现”- 不再是这种情况,成本太高:bugs.java.com/bugdatabase/view_bug.do?bug_id=6625725
_instance 的双重检查是否安全?我认为即使有挥发性,它们也不安全
“这将指示 JVM 线程从主内存中读取 volatile 变量的值,而不是在本地缓存它。”好点子
为了线程安全,也可以使用 private static final Singleton _instance;
@Chris311,当然,静态的 final 字段,它是线程安全的。
N
Nayantara Jeyaraj

volatile 对于停止线程非常有用。

并不是说您应该编写自己的线程,Java 1.6 有很多不错的线程池。但是如果你确定你需要一个线程,你需要知道如何停止它。

我用于线程的模式是:

public class Foo extends Thread {

  private volatile boolean close = false;

  public void run() {
    while(!close) {
      // do work
    }
  }
  public void close() {
    close = true;
    // interrupt here if needed
  }
}

在上述代码段中,while 循环中读取 close 的线程与调用 close() 的线程不同。如果没有 volatile,运行循环的线程可能永远不会看到要关闭的更改。

注意不需要同步


我想知道为什么这甚至是必要的。仅当其他线程必须以线程同步处于危险中的方式对该线程的状态更改做出反应时,才需要这样做吗?
@Jori,您需要 volatile ,因为在 while 循环中读取 close 的线程与调用 close() 的线程不同。如果没有 volatile,运行循环的线程可能永远不会看到要关闭的更改。
你会说停止这样的线程或使用 Thread#interrupt() 和 Thread#isInterrupted() 方法之间有优势吗?
@Pyrolistical - 您是否观察到线程从未看到实践中的变化?或者您可以扩展示例以可靠地触发该问题吗?我很好奇,因为我知道我使用(并且看到其他人使用)与示例基本相同但没有 volatile 关键字的代码,而且它似乎总是可以正常工作。
@aroth:使用今天的 JVM,您可以在实践中观察到,即使使用最简单的示例,您也无法可靠地重现此行为。对于更复杂的应用程序,您有时会在代码中执行具有内存可见性保证的其他操作,这使其碰巧起作用,这尤其危险,因为您不知道它为什么起作用,并且代码中一个简单的、明显不相关的更改可能会破坏您的应用…
S
Supun Wijerathne

使用 volatile 关键字声明的变量有两个使其特别的主要特性。

如果我们有一个 volatile 变量,它就不能被任何线程缓存到计算机的(微处理器)缓存中。访问总是从主存储器发生。如果对 volatile 变量进行写操作,突然请求读操作,则保证写操作将在读操作之前完成。

以上两个性质推断出

所有读取 volatile 变量的线程肯定会读取最新值。因为没有缓存值会污染它。并且只有在当前写操作完成后才会授予读请求。

而另一方面,

如果我们进一步研究我提到的#2,我们可以看到 volatile 关键字是维护共享变量的理想方法,该变量具有“n”个读取线程并且只有一个写入线程可以访问它。一旦我们添加了 volatile 关键字,它就完成了。没有任何其他关于线程安全的开销。

相反,

我们不能单独使用 volatile 关键字来满足多个写入线程访问它的共享变量


这解释了 volatile 和 synchronized 之间的区别。
可悲的是,这是不正确的。 “Volatile”不控制缓存,也不为其他 CPU 的内存视图提供任何神奇的即时全局更新。 “易失性”只是确保每当完成对变量的引用(读取或写入)时,JVM 都会执行对虚拟内存空间中变量分配地址的引用,而不是对存储在寄存器或其他一些中的值的引用优化器选择的方便的影子位置(如栈),也不会跳过优化器判断上的引用。
如果没有“volatile”,诸如“for (...) {a += b + c;}”之类的指令可能根本不会引用内存位置,只是将“a”、“b”和“c”保存在寄存器中循环的整个持续时间。当 CPU 将值写入虚拟内存地址(或就此而言,相应的物理内存地址)时,更新不会立即对其他 CPU 可见,也不会立即刷新到 RAM [*]。
更新只是简单地放到本地 CPU 的缓存中,然后排队到实现内存一致性协议的 CPU 间互连(例如 MESI),协议消息开始传播到其他 CPU,最终导致它们的缓存被更新也。这需要很短但非零的时间。与此同时,其他 CPU 仍然不知道发生了更新。如果 CPU1 更新了 volatile 变量 X,并且 CPU2 稍后读取它,CPU2 可能会找到 X 的旧值或 X 的新值。
在写入方面,“易失性”和“非易失性”之间的区别在于,对于“易失性”,CPU2 会在一纳秒左右后看到更新,而对于“非易失性”,更新延迟是不可预测的,取决于优化器。在读取方面,不同之处在于对于“易失性”,程序代码中对变量的引用会强制对虚拟内存中分配的变量位置的引用。而对于“非易失性”,优化器可能会选择跳过进行此类引用。
D
Dave L.

使用 volatile 的一个常见示例是使用 volatile boolean 变量作为标志来终止线程。如果您已经启动了一个线程,并且希望能够从另一个线程安全地中断它,您可以让该线程定期检查一个标志。要停止它,请将标志设置为 true。通过设置标志 volatile,您可以确保检查它的线程将在下次检查它时看到它已设置,甚至不必使用 synchronized 块。


y
ykaganovich

是的,只要您希望多个线程访问可变变量,就必须使用 volatile。这不是很常见的用例,因为通常您需要执行多个原子操作(例如,在修改变量之前检查变量状态),在这种情况下,您将使用同步块。


D
Donatello Boccaforno

没有人提到 long 和 double 变量类型的读写操作的处理。读取和写入对于引用变量和大多数原始变量都是原子操作,除了 long 和 double 变量类型,它们必须使用 volatile 关键字才能成为原子操作。 @link


为了更清楚,没有必要设置一个布尔值 volatile,因为布尔值的读取和写入已经是原子的。
@KaiWang 出于原子性目的,您不需要在布尔值上使用 volatile 。但是您当然可能出于知名度的原因。这就是你想说的吗?
y
yoAlex5

易挥发的

volatile -> synchronized[About]

volatile 对程序员说该值始终是最新的。问题是该值可以保存在不同类型的硬件内存中。例如,它可以是 CPU 寄存器、CPU 缓存、RAM ...

https://i.stack.imgur.com/1EPWG.png

volatile 关键字表示变量将直接从/向 RAM 内存读取和写入。它有一些计算足迹

Java 5 通过支持 happens-before[About] 扩展了 volatile

对 volatile 字段的写入发生在对该字段的每次后续读取之前。

Read is after write

volatile 关键字不能治愈 race condition 多个线程可以同时写入 一些值的情况。答案是synchronized关键字[About]

因此,仅当 一个 线程 写入 而其他线程仅读取 volatile 值时它才是安全的


W
Water

在我看来,除了停止使用 volatile 关键字的线程之外,还有两个重要的场景是:

双重检查锁定机制。在单例设计模式中经常使用。在这种情况下,需要将单例对象声明为 volatile。虚假唤醒。即使没有发出通知调用,线程有时也会从等待调用中唤醒。这种行为称为虚假唤醒。这可以通过使用条件变量(布尔标志)来解决。只要标志为真,就将 wait() 调用置于 while 循环中。因此,如果线程由于 Notify/NotifyAll 以外的任何原因从等待调用中唤醒,那么它遇到的标志仍然为真,因此再次调用等待。在调用 notify 之前将此标志设置为 true。在这种情况下,布尔标志被声明为 volatile。


整个#2 部分似乎很混乱,它把丢失的通知、虚假的唤醒和内存可见性问题混为一谈。此外,如果标志的所有用法都是同步的,那么 volatile 是多余的。我想我明白你的意思,但虚假唤醒不是正确的术语。请说清楚。
C
CJay

假设一个线程修改了共享变量的值,如果您没有为该变量使用 volatile 修饰符。当其他线程想要读取这个变量的值时,他们看不到更新的值,因为它们从 CPU 的缓存而不是 RAM 内存中读取变量的值。此问题也称为 Visibility Problem

通过声明共享变量 volatile,所有对计数器变量的写入都将立即写回主存。此外,所有对计数器变量的读取都将直接从主存储器中读取。

public class SharedObject {
    public volatile int sharedVariable = 0;
}

对于非易失性变量,无法保证 Java 虚拟机 (JVM) 何时将数据从主内存读取到 CPU 缓存,或将数据从 CPU 缓存写入主内存。这可能会导致几个问题,我将在以下部分中解释这些问题。

例子:

想象一下这样一种情况,两个或多个线程可以访问一个共享对象,该对象包含一个声明如下的计数器变量:

public class SharedObject {
    public int counter = 0;
}

也想象一下,只有线程 1 增加了计数器变量,但线程 1 和线程 2 都可能不时读取计数器变量。

如果计数器变量未声明为易失性,则无法保证计数器变量的值何时从 CPU 缓存写回主存。这意味着,CPU 缓存中的计数器变量值可能与主内存中的不同。这种情况如下所示:

https://i.stack.imgur.com/L57nd.png

线程没有看到变量的最新值的问题,因为它还没有被另一个线程写回主内存,被称为“可见性”问题。一个线程的更新对其他线程是不可见的。


主线程(父线程)通常会直接更新 ram 中的所有内容吗?还是主线程也是缓存
同样在Java中一般(非多线程场景),什么时候从缓存更新内存?
A
Arjan Tijms

如果您正在开发多线程应用程序,您将需要使用“volatile”关键字或“synchronized”以及您可能拥有的任何其他并发控制工具和技术。此类应用程序的示例是桌面应用程序。

如果您正在开发一个将部署到应用程序服务器(Tomcat、JBoss AS、Glassfish 等)的应用程序,您不必自己处理并发控制,因为它已经由应用程序服务器处理。事实上,如果我没记错的话,Java EE 标准禁止在 servlet 和 EJB 中进行任何并发控制,因为它是“基础设施”层的一部分,您应该无需处理它。如果您正在实现单例对象,则只能在此类应用程序中进行并发控制。如果您使用像 Spring 这样的框架来编织组件,这甚至已经解决了。

因此,在应用程序是 Web 应用程序并使用 Spring 或 EJB 等 IoC 框架的大多数 Java 开发情况下,您不需要使用“volatile”。


b
bwegs

volatile 只保证所有线程,甚至它们自己,都在递增。例如:计数器同时看到变量的同一面。它不是用来代替同步或原子或其他东西的,它完全使读取同步。请不要将其与其他 java 关键字进行比较。正如下面的示例所示,易失性变量操作也是原子的,它们会立即失败或成功。

package io.netty.example.telnet;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static volatile  int a = 0;
    public static void main(String args[]) throws InterruptedException{

        List<Thread> list = new  ArrayList<Thread>();
        for(int i = 0 ; i<11 ;i++){
            list.add(new Pojo());
        }

        for (Thread thread : list) {
            thread.start();
        }

        Thread.sleep(20000);
        System.out.println(a);
    }
}
class Pojo extends Thread{
    int a = 10001;
    public void run() {
        while(a-->0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.a++;
            System.out.println("a = "+Main.a);
        }
    }
}

即使您放 volatile 或 not 结果总是会有所不同。但是,如果您使用 AtomicInteger 如下结果将始终相同。这与同步也相同。

    package io.netty.example.telnet;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;

    public class Main {

        public static volatile  AtomicInteger a = new AtomicInteger(0);
        public static void main(String args[]) throws InterruptedException{

            List<Thread> list = new  ArrayList<Thread>();
            for(int i = 0 ; i<11 ;i++){
                list.add(new Pojo());
            }

            for (Thread thread : list) {
                thread.start();
            }

            Thread.sleep(20000);
            System.out.println(a.get());

        }
    }
    class Pojo extends Thread{
        int a = 10001;
        public void run() {
            while(a-->0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Main.a.incrementAndGet();
                System.out.println("a = "+Main.a);
            }
        }
    }

A
Abhishek Luthra

虽然我在这里提到的答案中看到了许多很好的理论解释,但我在这里添加了一个带有解释的实际示例:

1.

代码在没有易失性使用的情况下运行

public class VisibilityDemonstration {

private static int sCount = 0;

public static void main(String[] args) {
    new Consumer().start();
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        return;
    }
    new Producer().start();
}

static class Consumer extends Thread {
    @Override
    public void run() {
        int localValue = -1;
        while (true) {
            if (localValue != sCount) {
                System.out.println("Consumer: detected count change " + sCount);
                localValue = sCount;
            }
            if (sCount >= 5) {
                break;
            }
        }
        System.out.println("Consumer: terminating");
    }
}

static class Producer extends Thread {
    @Override
    public void run() {
        while (sCount < 5) {
            int localValue = sCount;
            localValue++;
            System.out.println("Producer: incrementing count to " + localValue);
            sCount = localValue;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                return;
            }
        }
        System.out.println("Producer: terminating");
    }
}
}

在上面的代码中,有两个线程——生产者和消费者。

生产者线程在循环中迭代 5 次(睡眠时间为 1000 毫秒或 1 秒)。在每次迭代中,生产者线程将 sCount 变量的值增加 1。因此,生产者在所有迭代中将 sCount 的值从 0 更改为 5

消费者线程处于一个恒定循环中,并在 sCount 的值发生变化时打印,直到该值达到 5 并结束。

两个循环同时启动。所以生产者和消费者都应该打印 sCount 的值 5 次。

输出

Consumer: detected count change 0
Producer: incrementing count to 1
Producer: incrementing count to 2
Producer: incrementing count to 3
Producer: incrementing count to 4
Producer: incrementing count to 5
Producer: terminating

分析

在上面的程序中,当生产者线程更新 sCount 的值时,它确实会更新主内存中的变量值(每个线程最初将要从中读取变量值的内存)。但是消费者线程第一次从主内存中读取 sCount 的值,然后将该变量的值缓存在自己的内存中。因此,即使主内存中原始 sCount 的值已被生产者线程更新,消费者线程仍在读取其未更新的缓存值。这称为可见性问题。

2.

使用易失性运行代码

在上面的代码中,将声明 sCount 的代码行替换为以下内容:

private volatile  static int sCount = 0;

输出

Consumer: detected count change 0
Producer: incrementing count to 1
Consumer: detected count change 1
Producer: incrementing count to 2
Consumer: detected count change 2
Producer: incrementing count to 3
Consumer: detected count change 3
Producer: incrementing count to 4
Consumer: detected count change 4
Producer: incrementing count to 5
Consumer: detected count change 5
Consumer: terminating
Producer: terminating

分析

当我们声明一个变量 volatile 时,这意味着对这个变量或从这个变量的所有读取和所有写入都将直接进入主存储器。这些变量的值永远不会被缓存。

由于 sCount 变量的值永远不会被任何线程缓存,因此消费者总是从主内存中读取 sCount 的原始值(它正在由生产者线程更新)。因此,在这种情况下,输出是正确的,两个线程都打印了 sCount 的不同值 5 次。

这样, volatile 关键字就解决了 VISIBILITY PROBLEM 问题。


有趣的是,当您在 if 语句之前将值打印出来时,消费者不会缓存该值。诡异的
这是一个很好的例子!
我理解在消费者线程中没有 volatile 关键字的情况下, sCount 值被读取一次并存储在本地,并且在消费者线程的整个生命周期中都会读取相同的值。但是,我们在 while() 循环检查中确实在生产者线程中读取了相同的 sCount 变量,生产者线程如何读取更新的值而不是缓存的值?
M
MB.

是的,我经常使用它——它对于多线程代码非常有用。你指出的文章是一篇好文章。虽然有两件重要的事情要记住:

如果您完全了解它的作用以及它与同步的不同之处,您应该只使用 volatile。在许多情况下,从表面上看,volatile 似乎是一种比 synchronized 更简单、性能更高的替代方案,而对 volatile 的更好理解通常会清楚地表明,synchronized 是唯一可行的选择。 volatile 在许多较旧的 JVM 中实际上并不工作,尽管 synchronized 可以。我记得看到一个文档引用了不同 JVM 中不同级别的支持,但不幸的是我现在找不到它。如果您使用的是 Java 1.5 之前的版本,或者您无法控制您的程序将在其上运行的 JVM,请务必考虑一下。


t
tstuber

每个访问 volatile 字段的线程都会在继续之前读取其当前值,而不是(可能)使用缓存值。

只有成员变量可以是易失的或瞬态的。


d
dgvid

绝对没错。 (不仅在 Java 中,在 C# 中也是如此。)有时您需要获取或设置一个值,该值保证是给定平台上的原子操作,例如 int 或 boolean,但不需要线程锁定的开销。 volatile 关键字允许您确保在读取值时获得当前值,而不是刚刚被另一个线程上的写入过时的缓存值。


M
Mohan

volatile 关键字有两种不同的用法。

阻止 JVM 从寄存器中读取值(假设为缓存),并强制从内存中读取其值。降低内存不一致错误的风险。

防止 JVM 读取寄存器中的值,并强制从内存中读取其值。

繁忙标志用于防止线程在设备繁忙且该标志不受锁保护时继续:

while (busy) {
    /* do something else */
}

当另一个线程关闭忙标志时,测试线程将继续:

busy = 0;

但是,由于busy在测试线程中被频繁访问,JVM可以通过将busy的值放在一个寄存器中来优化测试,然后在每次测试之前不读取内存中busy的值就测试寄存器的内容。测试线程永远不会看到busy变化,而另一个线程只会改变内存中busy的值,从而导致死锁。将繁忙标志声明为 volatile 会强制在每次测试之前读取其值。

降低内存一致性错误的风险。

使用 volatile 变量可以降低内存一致性错误的风险,因为对 volatile 变量的任何写入都会与后续读取该相同变量建立“先发生”关系。这意味着对 volatile 变量的更改始终对其他线程可见。

没有内存一致性错误的读写技术称为原子动作。

原子动作是一次有效地发生的动作。原子动作不能在中间停止:它要么完全发生,要么根本不发生。在操作完成之前,原子操作的副作用是不可见的。

以下是您可以指定的原子操作:

对于引用变量和大多数原始变量(除了 long 和 double 之外的所有类型),读取和写入都是原子的。

对于声明为 volatile 的所有变量(包括 long 和 double 变量),读取和写入都是原子的。

干杯!


s
sankar banerjee

挥发物会跟随。

1> 不同线程对volatile变量的读写总是来自内存,而不是来自线程自己的缓存或者cpu寄存器。所以每个线程总是处理最新的值。 2> 当 2 个不同的线程在堆中使用相同的实例或静态变量时,可能会认为其他线程的操作是无序的。请参阅 jeremy manson 的博客。但是 volatile 在这里有所帮助。

以下完全运行的代码显示了如何在不使用同步关键字的情况下按预定义的顺序执行多个线程并打印输出。

thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3
thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3
thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3

为了实现这一点,我们可以使用以下完整的运行代码。

public class Solution {
    static volatile int counter = 0;
    static int print = 0;
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Thread[] ths = new Thread[4];
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new Thread(new MyRunnable(i, ths.length));
            ths[i].start();
        }
    }
    static class MyRunnable implements Runnable {
        final int thID;
        final int total;
        public MyRunnable(int id, int total) {
            thID = id;
            this.total = total;
        }
        @Override
        public void run() {
            // TODO Auto-generated method stub
            while (true) {
                if (thID == counter) {
                    System.out.println("thread " + thID + " prints " + print);
                    print++;
                    if (print == total)
                        print = 0;
                    counter++;
                    if (counter == total)
                        counter = 0;
                } else {
                    try {
                        Thread.sleep(30);
                    } catch (InterruptedException e) {
                        // log it
                    }
                }
            }
        }
    }
}

以下 github 链接有一个自述文件,其中给出了正确的解释。 https://github.com/sankar4git/volatile_thread_ordering


C
Community

从 oracle 文档 page 中,需要 volatile 变量来解决内存一致性问题:

使用 volatile 变量可以降低内存一致性错误的风险,因为对 volatile 变量的任何写入都会与后续读取该相同变量建立起先发生关系。

这意味着对 volatile 变量的更改始终对其他线程可见。这也意味着当线程读取 volatile 变量时,它不仅会看到 volatile 的最新更改,还会看到导致更改的代码的副作用。

正如 Peter Parker 答案中所解释的,在没有 volatile 修饰符的情况下,每个线程的堆栈都可能有自己的变量副本。通过将变量设为 volatile,内存一致性问题已得到修复。

查看 jenkov 教程页面以获得更好的理解。

查看相关的 SE 问题,了解有关 volatile 和使用 volatile 的用例的更多详细信息:

Difference between volatile and synchronized in Java

一个实际用例:

您有许多线程,需要以特定格式打印当前时间,例如:java.text.SimpleDateFormat("HH-mm-ss")。 Yon 可以有一个类,它将当前时间转换为 SimpleDateFormat 并每隔一秒更新一次变量。所有其他线程可以简单地使用这个 volatile 变量在日志文件中打印当前时间。


N
Neha Vari

易失性变量是轻量级同步。当所有线程中最新数据的可见性是必需的并且原子性可能会受到损害时,在这种情况下必须首选易失性变量。对 volatile 变量的读取总是返回任何线程最近完成的写入,因为它们既没有缓存在寄存器中,也没有缓存在其他处理器看不到的缓存中。易失性是无锁的。当场景符合上述标准时,我使用 volatile。


N
Niyaz Ahamad

volatile 变量在更新后基本上用于主共享缓存行中的即时更新(刷新),以便立即反映到所有工作线程。


J
Java Main

volatile 键与变量一起使用时,将确保读取此变量的线程将看到相同的值。现在,如果您有多个线程读取和写入一个变量,则使变量 volatile 是不够的,并且数据将被损坏。图像线程读取了相同的值,但每个线程都进行了一些更改(比如增加了一个计数器),当写回内存时,数据完整性被破坏。这就是为什么有必要使变量同步(不同的方式是可能的)

如果更改由 1 个线程完成,而其他线程只需要读取此值,则 volatile 将是合适的。


m
manikanta

下面是一个非常简单的代码来演示 volatile 对变量的要求,该变量用于控制来自其他线程的线程执行(这是需要 volatile 的一个场景)。

// Code to prove importance of 'volatile' when state of one thread is being mutated from another thread.
// Try running this class with and without 'volatile' for 'state' property of Task class.
public class VolatileTest {
    public static void main(String[] a) throws Exception {
        Task task = new Task();
        new Thread(task).start();

        Thread.sleep(500);
        long stoppedOn = System.nanoTime();

        task.stop(); // -----> do this to stop the thread

        System.out.println("Stopping on: " + stoppedOn);
    }
}

class Task implements Runnable {
    // Try running with and without 'volatile' here
    private volatile boolean state = true;
    private int i = 0;

    public void stop() {
        state = false;
    } 

    @Override
    public void run() {
        while(state) {
            i++;
        }
        System.out.println(i + "> Stopped on: " + System.nanoTime());
    }
}

如果不使用 volatile即使在“Stopped on: xxx”之后,您也永远不会看到“Stopped on: xxx”消息,并且程序继续运行。

Stopping on: 1895303906650500

使用 volatile 时:您会立即看到“Stopped on: xxx”。

Stopping on: 1895285647980000
324565439> Stopped on: 1895285648087300

演示:https://repl.it/repls/SilverAgonizingObjectcode


投反对票:愿意解释为什么投反对票?如果这不是真的,至少我会知道什么是错的。我已经添加了两次相同的评论,但不知道是谁一次又一次地删除