今天工作时,我在Java中遇到了volatile关键字。不是很熟悉,我找到了这个解释。

鉴于那篇文章详细解释了所讨论的关键字,您曾经使用过它吗?或者您曾经看到过以正确方式使用该关键字的情况吗?


当前回答

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

1.

代码在不使用volatile的情况下运行

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

分析

In the above program, when the producer thread updates the value of sCount, it does update the value of the variable in the main memory(memory from where every thread is going to initially read the value of variable). But the consumer thread reads the value of sCount only the first time from this main memory and then caches the value of that variable inside its own memory. So, even if the value of original sCount in main memory has been updated by the producer thread, the consumer thread is reading from its cached value which is not updated. This is called VISIBILITY PROBLEM .

2.

代码使用volatile运行

在上面的代码中,用下面的代码替换声明了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的原始值(在那里由生产者线程更新)。因此,在这种情况下,输出是正确的,两个线程都打印了5次不同的sCount值。

通过这种方式,volatile关键字解决了可见性问题。

其他回答

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

If you are developing an application that would be deployed to application server (Tomcat, JBoss AS, Glassfish, etc) you don't have to handle concurrency control yourself as it already addressed by the application server. In fact, if I remembered correctly the Java EE standard prohibit any concurrency control in servlets and EJBs, since it is part of the 'infrastructure' layer which you supposed to be freed from handling it. You only do concurrency control in such app if you're implementing singleton objects. This even already addressed if you knit your components using frameworkd like Spring.

因此,在Java开发的大多数情况下,应用程序是一个web应用程序,并使用IoC框架,如Spring或EJB,你不需要使用'volatile'。

是的,当你想要一个可变变量被多个线程访问时,必须使用volatile。这不是很常见的用例,因为通常你需要执行不止一个原子操作(例如,在修改变量之前检查变量状态),在这种情况下,你会使用同步块代替。

下面是一个非常简单的代码,演示了volatile for variable的需求,它用于从其他线程控制线程执行(这是需要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时:即使在“stop on: xxx”之后,你也永远不会看到“Stopped on: xxx”消息,并且程序继续运行。

Stopping on: 1895303906650500

当使用volatile时:你会立即看到'Stopped on: xxx'。

Stopping on: 1895285647980000
324565439> Stopped on: 1895285648087300

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

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

从oracle文档页,需要volatile变量来修复内存一致性问题:

使用易失性变量可以降低内存一致性错误的风险,因为对易失性变量的任何写入都会与该变量的后续读取建立happens-before关系。

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

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

为了更好地理解,请查看jenkov教程页面。

看一下相关的SE问题,了解更多关于volatile的细节和使用volatile的用例:

Java中volatile和synchronized的区别

一个实际的用例:

你有很多线程,它们需要以特定的格式打印当前时间,例如:java.text.SimpleDateFormat("HH-mm-ss")。可以有一个类,它将当前时间转换为SimpleDateFormat,并每一秒更新一次变量。所有其他线程都可以使用这个易失性变量在日志文件中打印当前时间。