Java中volatile关键字的最简单易懂的例子

时间:2013-07-19 14:02:34

标签: java multithreading concurrency volatile

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

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

以下代码段无效(取自here):

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的理论。
  • 是同步的易失性替代品吗?它是否实现了原子性?

13 个答案:

答案 0 :(得分:45)

挥发性 - >保证可见性而非原子性

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

易失性不能代替同步

仅在更新引用且不对其执行某些其他操作时使用volatile。

示例:

volatile int i = 0;

public void incrementI(){
   i++;
}
如果不使用同步或AtomicInteger,

将不是线程安全的,因为递增是复合操作。

  

为什么程序无法无限期运行?

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

Correct use of volatile讨论了volatile的各种可能用法。正确使用volatile非常棘手,我会说“如果有疑问,请将其保留”,请改用synchronized块。

此外:

可以使用synchronized块代替volatile,但反之则不正确

答案 1 :(得分:25)

对于您的特定示例:如果未声明为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变量:

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

调试提示:确保在调用JVM时始终指定-server JVM命令行开关,即使是用于开发和测试。服务器JVM执行比客户端JVM更多的优化,例如从循环中提取变量而不在循环中修改;可能似乎在开发环境(客户端JVM)中工作的代码可能会在部署环境中中断 (服务器JVM)。

这是"Java Concurrency in Practice"的摘录,这是你在这个主题上可以找到的最好的书。

答案 2 :(得分:13)

我稍微修改了你的例子。现在使用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);
    }
}

答案 3 :(得分:10)

什么是volatile关键字?

  

易变关键字会阻止caching of variables

考虑代码,首先是没有易失性关键字

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();   
    }    
}

理想情况,此程序应print hello,直到RETURN key被按下。但是在some machines上可能会发生变量正在运行cached并且您无法从shutdown()方法更改其值,这会导致infinite打印hello文本。

因此,使用volatile关键字,guaranteed不会缓存您的变量,即run fine上的all machines

private volatile boolean running = true;  //volatile keyword

因此,使用volatile关键字是goodsafer programming practice

答案 4 :(得分:6)

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

Access_Modifier volatile DataType Variable_Name;

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

enter image description here

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

  

volatile cannot be cached - assembler

    +--------------+--------+-------------------------------------+
    |  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: 可以在线程之间共享的内存称为共享内存或堆内存。所有实例字段,静态字段和数组元素都存储在堆内存中。

Synchronization:synchronized适用于方法,块。允许在对象上一次只执行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();
        }
    }
}
  

静态[Class Field] vs 易失性[Instance Field] - 两者都不会被线程缓存

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

  •   
  • Volatile主要用于存储在堆区域的实例变量。 volatile的主要用途是维护所有线程的更新值。实例volatile字段可以是Serialized

  •   

@see

答案 5 :(得分:3)

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

答案 6 :(得分:2)

  

理想情况下,如果keepRunning不易变,则线程应该无限期地继续运行。但是,它会在几秒钟后停止。

如果您在单处理器中运行或者您的系统非常繁忙,则操作系统可能会交换导致某些级别的缓存失效的线程。正如其他人所提到的,没有volatile并不意味着内存将被共享,但是如果出于性能原因,JVM正在尝试不同步内存,那么内存可能没有更新。

需要注意的另一件事是System.out.println(...)已同步,因为基础PrintStream执行同步以停止重叠输出。所以你得到了内存同步&#34;免费&#34;在主线程中。这仍然无法解释为什么读取循环会看到更新。

println(...)行是否进入,你的程序在带有Intel i7的MacBook Pro上的Java6下旋转。

  

任何人都可以用例子解释volatile吗?不是JLS的理论。

我认为你的榜样很好。不确定为什么它不能删除所有System.out.println(...)个语句。它对我有用。

  

是同步的易失性替代品吗?它是否实现了原子性?

就内存同步而言,volatile会引发与synchronized块相同的内存屏障,除了volatile屏障是单向和双向的。 volatile读取会引发加载障碍,而写入会引发商店障碍。 synchronized区块是双向障碍。

然而,就atomicity而言,答案是&#34;它取决于&#34;。如果您正在从字段中读取或写入值,则volatile会提供正确的原子性。但是,增加volatile字段会受到++实际上是3个操作的限制:读取,增量,写入。在这种情况下或更复杂的互斥锁情况下,可能需要一个完整的synchronized块。

答案 7 :(得分:2)

volatile不一定会创建巨大的变化,具体取决于JVM和编译器。但是,对于许多(边缘)情况,可能是优化导致变量的更改无法被注意到而不是正确编写它们。

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

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

答案 8 :(得分: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以获得更清晰的信息。

答案 9 :(得分: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的示例。

答案 10 :(得分:1)

很多很好的例子,但我只是想补充一点,在很多情况下都需要使用volatile,因此没有一个具体的例子可以对它们进行裁定。

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

查看更多Java volatile examples

答案 11 :(得分:0)

声明为volatile的对象通常用于在线程之间传递状态信息,以确保CPU缓存更新,即保持同步,存在易失性字段,CPU指令,内存屏障,通常称为发出一个membar或fence,来更新CPU缓存,并更改volatile字段的值。

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

volatile变量只能在Thread Context中使用。请参阅示例here

答案 12 :(得分:0)

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();

    }
}