82

我正在阅读Java 中的volatile关键字并完全理解它的理论部分。

但是,我正在寻找的是一个很好的案例示例,它显示了如果变量不是volatile并且如果它是 volatile 会发生什么。

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

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不是volatile,线程应该无限期地继续运行。但是,它确实会在几秒钟后停止。

我有两个基本问题:

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

13 回答 13

53

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

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

易失性不能替代同步

仅当您更新引用而不对其执行一些其他操作时才使用 volatile。

例子:

volatile int i = 0;

public void incrementI(){
   i++;
}

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

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

嗯,这取决于各种情况。在大多数情况下,JVM 足够聪明,可以刷新内容。

正确使用 volatile讨论了 volatile 的各种可能用途。正确使用 volatile 很棘手,我会说“如果有疑问,请不要使用它”,请改用同步块。

还:

synchronized 块可以用来代替 volatile 但反之则不然

于 2013-07-19T14:07:04.783 回答
28

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

关于 volatile 变量的一般解释如下:

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

volatile 变量的可见性影响超出了 volatile 变量本身的值。当线程 A 写入 volatile 变量并且随后线程 B 读取相同的变量时,在写入 volatile 变量之前对 A 可见的所有变量的值在读取 volatile 变量后对 B 可见。

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

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

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

加锁可以同时保证可见性和原子性;volatile 变量只能保证可见性。

只有满足以下所有条件时,才能使用 volatile 变量:

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

调试提示:确保-server在调用 JVM 时始终指定 JVM 命令行开关,即使是用于开发和测试。服务端 JVM 比客户端 JVM 执行更多的优化,比如将循环中没有被修改的变量提升到循环之外;看起来在开发环境(客户端 JVM)中工作的代码可能会在部署环境(服务器 JVM)中中断。

这是“Java Concurrency in Practice”的摘录,这是您可以找到的关于该主题的最佳书籍。

于 2013-07-19T14:24:11.510 回答
17

我稍微修改了你的例子。现在将keepRunning 用作易失性和非易失性成员的示例:

class TestVolatile extends Thread{
    //volatile
    boolean keepRunning = true;

    public void run() {
        long count=0;
        while (keepRunning) {
            count++;
        }

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

    public static void main(String[] args) throws InterruptedException {
        TestVolatile t = new TestVolatile();
        t.start();
        Thread.sleep(1000);
        System.out.println("after sleeping in main");
        t.keepRunning = false;
        t.join();
        System.out.println("keepRunning set to " + t.keepRunning);
    }
}
于 2014-01-05T10:02:52.347 回答
17

volatile关键字是什么?volatile关键字防止缓存变量。

考虑这段代码,首先没有volatile关键字:

class MyThread extends Thread {
    private boolean running = true;   //non-volatile keyword

    public void run() {
        while (running) {
            System.out.println("hello");
        }
    }

    public void shutdown() {
        running = false;
    }
}

public class Main {

    public static void main(String[] args) {
        MyThread obj = new MyThread();
        obj.start();

        Scanner input = new Scanner(System.in);
        input.nextLine(); 
        obj.shutdown();   
    }    
}

理想情况下,该程序应该一直打印helloReturn按键被按下。但是在某些机器上,可能会发生变量running被缓存并且您无法从shutdown()导致无限打印hello文本的方法中更改其值的情况。

因此,通过使用volatile关键字,可以保证您的变量不会被缓存,并且代码将在所有机器上正常运行。

private volatile boolean running = true;  //volatile keyword

使用volatile关键字是一种良好且更安全的编程习惯。

于 2015-07-18T20:08:14.213 回答
7

Ideally, if keepRunning wasn't volatile, thread should keep on running indefinitely. But, it does stop after few seconds.

If you are running in a single-processor or if your system is very busy, the OS may be swapping out the threads which causes some levels of cache invalidation. Not having a volatile doesn't mean that memory will not be shared, but the JVM is trying to not synchronize memory if it can for performance reasons so the memory may not be updated.

Another thing to note is that System.out.println(...) is synchronized because the underlying PrintStream does synchronization to stop overlapping output. So you are getting memory synchronization "for free" in the main-thread. This still doesn't explain why the reading loop sees the updates at all however.

Whether the println(...) lines are in or out, your program spins for me under Java6 on a MacBook Pro with an Intel i7.

Can anyone explain volatile with example ? Not with theory from JLS.

I think your example is good. Not sure why it isn't working with all System.out.println(...) statements removed. It works for me.

Is volatile substitute for synchronization ? Does it achieve atomicity ?

In terms of memory synchronization, volatile throws up the same memory barriers as a synchronized block except that the volatile barrier is uni-directional versus bi-directional. volatile reads throw up a load-barrier while writes throw up a store-barrier. A synchronized block is a bi-directional barrier with the addition of mutex locking.

In terms of atomicity, however, the answer is "it depends". If you are reading or writing a value from a field then volatile provides proper atomicity. However, incrementing a volatile field suffers from the limitation that ++ is actually 3 operations: read, increment, write. In that case or more complex mutex cases, a full synchronized block may be necessary. AtomicInteger solves the ++ issue with a complicated test-and-set spin-loop.

于 2013-07-19T14:04:25.193 回答
7

Variable Volatile:Volatile 关键字适用于变量。Java 中的 volatile 关键字保证 volatile 变量的值总是从主内存中读取,而不是从 Thread 的本地缓存中读取。

Access_Modifier volatile DataType Variable_Name;

易失性字段:向 VM 指示多个线程可能尝试同时访问/更新该字段的值。一种特殊的实例变量,它必须在所有线程之间共享,具有修改的值。与静态(类)变量类似,只有一个易失性值的副本缓存在主存储器中,因此在执行任何 ALU 操作之前,每个线程都必须在 ALU 操作之后从主存储器读取更新的值,它必须直接写入主存储器。(对 volatile 变量 v 的写入与任何线程对 v 的所有后续读取同步)这意味着对 volatile 变量的更改始终对其他线程可见。

在此处输入图像描述

如果线程 t1 更改了 t1 缓存中nonvoltaile variable的值,线程 t2 无法访问更改的值,直到 t1 写入,t2 从主内存读取最近修改的值,这可能导致Data-Inconsistancy.

volatile 不能被缓存-汇编器

    +--------------+--------+-------------------------------------+
    |  Flag Name   |  Value | Interpretation                      |
    +--------------+--------+-------------------------------------+
    | ACC_VOLATILE | 0x0040 | Declared volatile; cannot be cached.|
    +--------------+--------+-------------------------------------+
    |ACC_TRANSIENT | 0x0080 | Declared transient; not written or  |
    |              |        | read by a persistent object manager.|
    +--------------+--------+-------------------------------------+

Shared Variables:可以在线程之间共享的内存称为共享内存或堆内存。所有实例字段、静态字段和数组元素都存储在堆内存中。

同步:同步适用于方法、块。允许在对象上一次只执行 1 个线程。如果 t1 获得控制权,那么剩余线程必须等待直到它释放控制权。

例子:

public class VolatileTest implements Runnable {

    private static final int MegaBytes = 10241024;

    private static final Object counterLock = new Object();
    private static int counter = 0;
    private static volatile int counter1 = 0;

    private volatile int counter2 = 0;
    private int counter3 = 0;

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            concurrentMethodWrong();
        }

    }

    void addInstanceVolatile() {
        synchronized (counterLock) {
            counter2 = counter2 + 1;
            System.out.println( Thread.currentThread().getName() +"\t\t « InstanceVolatile :: "+ counter2);
        }
    }

    public void concurrentMethodWrong() {
        counter = counter + 1;
        System.out.println( Thread.currentThread().getName() +" « Static :: "+ counter);
        sleepThread( 1/4 );

        counter1 = counter1 + 1;
        System.out.println( Thread.currentThread().getName() +"\t « StaticVolatile :: "+ counter1);
        sleepThread( 1/4 );

        addInstanceVolatile();
        sleepThread( 1/4 );

        counter3 = counter3 + 1;
        sleepThread( 1/4 );
        System.out.println( Thread.currentThread().getName() +"\t\t\t\t\t « Instance :: "+ counter3);
    }
    public static void main(String[] args) throws InterruptedException {
        Runtime runtime = Runtime.getRuntime();

        int availableProcessors = runtime.availableProcessors();
        System.out.println("availableProcessors :: "+availableProcessors);
        System.out.println("MAX JVM will attempt to use : "+ runtime.maxMemory() / MegaBytes );
        System.out.println("JVM totalMemory also equals to initial heap size of JVM : "+ runtime.totalMemory() / MegaBytes );
        System.out.println("Returns the amount of free memory in the JVM : "+ untime.freeMemory() / MegaBytes );
        System.out.println(" ===== ----- ===== ");

        VolatileTest volatileTest = new VolatileTest();
        Thread t1 = new Thread( volatileTest );
        t1.start();

        Thread t2 = new Thread( volatileTest );
        t2.start();

        Thread t3 = new Thread( volatileTest );
        t3.start();

        Thread t4 = new Thread( volatileTest );
        t4.start();

        Thread.sleep( 10 );;

        Thread optimizeation = new Thread() {
            @Override public void run() {
                System.out.println("Thread Start.");

                Integer appendingVal = volatileTest.counter2 + volatileTest.counter2 + volatileTest.counter2;

                System.out.println("End of Thread." + appendingVal);
            }
        };
        optimizeation.start();
    }

    public void sleepThread( long sec ) {
        try {
            Thread.sleep( sec * 1000 );
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Static[ Class Field] vs Volatile[ Instance Field] - 两者都没有被线程缓存

  • 静态字段对所有线程都是通用的,并存储在方法区中。static 与 volatile 没用。静态字段无法序列化。

  • Volatile 主要与存储在堆区域中的实例变量一起使用。volatile 的主要用途是维护所有线程的更新值。实例 volatile 字段可以被序列化

@看

于 2017-12-07T13:02:43.893 回答
4

当一个变量是volatile时,它保证它不会被缓存并且不同的线程将看到更新的值。但是,不标记它volatile并不能保证相反。volatile是在 JVM 中被破坏了很长时间但仍然没有被很好理解的东西之一。

于 2013-07-19T14:05:51.880 回答
2

volatile不一定会产生巨大的变化,具体取决于 JVM 和编译器。但是,对于许多(边缘)情况,优化导致变量的更改未能被注意到与正确写入它们之间可能存在差异。

基本上,优化器可以选择将非易失性变量放在寄存器或堆栈上。如果另一个线程在堆或类的原语中更改了它们,另一个线程将继续在堆栈上查找它,并且它会过时。

volatile确保不会发生此类优化,并且所有读取和写入都直接到堆或所有线程都会看到的其他地方。

于 2013-07-19T14:05:40.097 回答
2

很多很好的例子,但我只想补充一点,有很多场景volatile需要,所以没有一个具体的例子来统治它们。

  1. 您可以使用volatile强制所有线程从主内存中获取变量的最新值。
  2. 您可以synchronization用来保护关键数据
  3. 您可以使用LockAPI
  4. 您可以使用Atomic变量

查看更多Java volatile 示例

于 2019-08-05T20:57:47.053 回答
1

请在下面找到解决方案,

此变量的值永远不会在线程本地缓存:所有读取和写入都将直接进入“主内存”。volatile 强制线程每次更新原始变量。

public class VolatileDemo {

    private static volatile int MY_INT = 0;

    public static void main(String[] args) {

        ChangeMaker changeMaker = new ChangeMaker();
        changeMaker.start();

        ChangeListener changeListener = new ChangeListener();
        changeListener.start();

    }

    static class ChangeMaker extends Thread {

        @Override
        public void run() {
            while (MY_INT < 5){
                System.out.println("Incrementing MY_INT "+ ++MY_INT);
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException exception) {
                    exception.printStackTrace();
                }
            }
        }
    }

    static class ChangeListener extends Thread {

        int local_value = MY_INT;

        @Override
        public void run() {
            while ( MY_INT < 5){
                if( local_value!= MY_INT){
                    System.out.println("Got Change for MY_INT "+ MY_INT);
                    local_value = MY_INT;
                }
            }
        }
    }

}

请参阅此链接http://java.dzone.com/articles/java-volatile-keyword-0以获得更清晰的信息。

于 2014-10-28T13:43:28.187 回答
1
public class VolatileDemo {
    static class Processor {
        //without volatile program keeps running on my platform
        private boolean flag = false;

        public void setFlag() {
            System.out.println("setting flag true");
            this.flag = true;
        }

        public void process() {
            while(!flag) {
                int x = 5;
                // using sleep or sout will end the program without volatile.
                // Probably these operations, cause thread to be rescheduled, read from memory. Thus read new flag value and end.
            }

            System.out.println("Ending");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Processor processor = new Processor();
        Thread t1 = new Thread(processor::process);

        t1.start();

        Thread.sleep(2000);
        processor.setFlag();

    }
}
于 2021-05-07T15:39:39.867 回答
1

volatile关键字告诉JVM它可能被另一个线程修改。每个线程都有自己的堆栈,因此它可以访问自己的变量副本。创建线程时,它会将所有可访问变量的值复制到自己的内存中。

public class VolatileTest {
    private static final Logger LOGGER = MyLoggerFactory.getSimplestLogger();

    private static volatile int MY_INT = 0;

    public static void main(String[] args) {
        new ChangeListener().start();
        new ChangeMaker().start();
    }

    static class ChangeListener extends Thread {
        @Override
        public void run() {
            int local_value = MY_INT;
            while ( local_value < 5){
                if( local_value!= MY_INT){
                    LOGGER.log(Level.INFO,"Got Change for MY_INT : {0}", MY_INT);
                     local_value= MY_INT;
                }
            }
        }
    }

    static class ChangeMaker extends Thread{
        @Override
        public void run() {

            int local_value = MY_INT;
            while (MY_INT <5){
                LOGGER.log(Level.INFO, "Incrementing MY_INT to {0}", local_value+1);
                MY_INT = ++local_value;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }
    }
}

在有和没有 volatile 的情况下尝试这个例子。

于 2019-02-08T06:31:07.903 回答
0

声明为 volatile 的对象通常用于在线程之间传递状态信息,以确保更新 CPU 缓存,即保持同步,在存在 volatile 字段的情况下,CPU 指令、内存屏障,通常称为 membar 或栅栏,被发出来更新 CPU 缓存,改变 volatile 字段的值。

volatile 修饰符告诉编译器,由 volatile 修改的变量可能会被程序的其他部分意外更改。

volatile 变量只能在线程上下文中使用。看这里的例子

于 2013-10-28T19:49:18.010 回答