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


75

我正在阅读有关Java中的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,则线程应无限期继续运行。但是,它确实会在几秒钟后停止。

我有两个基本问题:

  • 谁能用例子解释易失性?不符合JLS的理论。
  • 易失性替代了同步吗?它能达到原子性吗?

过去的一篇文章广泛讨论了它stackoverflow.com/questions/7212155/java-threading-volatile
AurA

4
你在倒想。理想情况下,如果keepRunning不可变,则线程应无限期继续运行。实际上,情况恰恰相反:添加volatile 保证字段的更改将可见。没有关键字,根本就没有任何保证,任何事情都会发生。您无法声明线程应继续运行
布鲁诺·里斯

3
事情就是这样:内存可见性错误本质上很难(不可能?)通过一个简单的示例来演示,该示例每次都会失败。假设您有一台多核计算机,那么如果您多次运行(例如1000次运行),您的示例可能至少会失败几次。如果您有一个大程序(例如,整个程序及其对象不适合CPU缓存),那么这会增加发现错误的可能性。基本上,并发错误是这样的,如果理论上说它可以破坏,它可能会破坏,但是每几个月只能一次,并且可能在生产中。
yshavit

有一个很好的例子已经列出了stackoverflow.com/questions/5816790/…–
gurubelli

Answers:


51

易挥发->确保可见性和非原子性

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

易失性不能替代同步

仅在更新参考并且不对其执行一些其他操作时,才使用volatile。

例:

volatile int i = 0;

public void incrementI(){
   i++;
}

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

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

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

正确使用volatile讨论了volatile的各种可能用法。正确使用volatile是很棘手的,我会说“有疑问时,请不要使用它”,而应使用同步块。

也:

可以使用同步块代替volatile,但取反是不正确的


4
错了 挥发物保证原子性质。Oracle文档明确规定了这一点。请参阅docs.oracle.com/javase/tutorial/essential/concurrency/…
Kalpa Gunarathna '16

4
在Java中,当我们有多个线程时,每个线程都有自己的堆栈(内存空间),并且每个线程都有一个自己可以访问的变量副本。如果不存在volatile关键字来修饰int i,则每个线程都可以在其执行中使用它。当用volatile声明时,每个线程都必须直接从主内存读取i的值,而不是从本地副本读取i的值。因此,从每个线程的角度来看,对变量i的操作都是原子的。
Kalpa Gunarathna

atomicity答案的一部分令人困惑。同步为您提供互斥的访问权限可见性volatile仅提供可见性。也volatile使的读/写longdouble原子(同步做它太受它的互斥性质)。
IlyaEremin '19

27

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

有关易失性变量的一般说明如下:

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

易失性变量的可见性影响超出了易失性变量本身的值。当线程A写入易失性变量,然后线程B读取相同的变量时,在写入易失性变量之前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”的摘录,您可以找到有关此主题的最佳书籍。


15

我已经稍微修改了您的示例。现在使用将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);
    }
}

很好的例子。这对我很有效。在keepRunning上没有挥发的线程永远挂起。一旦将keepRunning标记为volatile-它会在t.keepRunning = false
鲍里斯(Boris)2015年

4
样例对我有用,一直在寻找样例。+1是因为它对我有帮助,而且缺乏解释并没有损害也没有应受的否决。
John Doe

1
paritosht和@John Doe,您好,您能否解释一下为什么您的代码是可行的示例?当我的机器执行问题中提供的代码(带有或不带有volatile关键字)时,它始终会停止。
shanwu

我在votalite这里和一起得到相同的结果
WW

13

什么是volatile关键字?

volatile关键字阻止caching of variables

考虑代码,首先没有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();   
    }    
}

理想情况下,该程序应print hello一直RETURN key被按下。但是,some machines可能正在运行的变量可能是cached并且您无法通过shutdown()方法更改其值,从而导致infinite打印问候文本。

因此,使用volatile关键字,这是guaranteed您的变量将不会被缓存,即会run fineall machines

private volatile boolean running = true;  //volatile keyword

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


7

Variable Volatile:易变关键字适用于变量。Java中的volatile关键字保证了volatile变量的值将始终从主内存而不是从线程的本地缓存中读取。

Access_Modifier volatile DataType Variable_Name;

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

在此处输入图片说明

这里一个nonvoltaile variable,如果线程T1变化T1的缓存值,线程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();
        }
    }
}

静态[ Class Field]挥发性[ Instance Field]-两者都不被线程缓存

  • 静态字段是所有线程共有的,并存储在“方法区域”中。静态与挥发性无用。静态字段无法序列化。

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

@看到


6

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

如果您在单处理器中运行,或者系统非常繁忙,则操作系统可能会换出线程,从而导致某些级别的缓存失效。没有avolatile并不意味着不会共享内存,但是JVM出于性能原因会尝试不同步内存(如果可以),因此可能不会更新内存。

要注意的另一件事System.out.println(...)是同步,因为基础进行PrintStream同步以停止重叠的输出。因此,您可以在主线程中“免费”获得内存同步。但是,这仍然不能解释为什么阅读循环完全看到更新。

无论println(...)是输入输出还是输出输出,您的程序都可以在配备Intel i7的MacBook Pro上以Java6操作系统为我旋转。

任何人都可以用示例解释volatile吗?不符合JLS的理论。

我认为您的榜样很好。不知道为什么它不能在System.out.println(...)删除所有语句的情况下工作。这个对我有用。

易失性可以代替同步吗?它能达到原子性吗?

在内存同步方面,volatile抛出的内存屏障与synchronized块相同,除了volatile屏障是单向的还是双向的。 volatile读引发负载障碍,写引发存储障碍。阿synchronized块是双向屏障通过加入互斥锁定。

在方面atomicity,然而,答案是“看情况”。如果要从字段读取或写入值,则volatile可以提供适当的原子性。但是,递增volatile字段受到++实际上是3个操作的限制:读取,递增,写入。在那种情况或更复杂的互斥情况下,synchronized可能需要一个完整的块。 AtomicInteger通过++复杂的测试和设置自旋环解决了该问题。


我评论了两个SOPln语句,但是几秒钟后它仍然停止了..您能给我看一个可以按预期工作的示例吗?
tmgr

您是否在@ tm99单处理器系统上运行?因为您的程序会在Macbook Pro Java6上永远为我旋转。
2013年

我在Win Xp 32位Java 6上运行
tmgr

2
“任何同步块(或任何易失性字段)都会导致所有内存同步”-确定吗?您会提供JLS参考吗?据我所知,唯一的保证是,在释放锁L1之前执行的对内存的修改对于获得相同锁L1的线程是可见的。对于volatile,在对同一字段F1进行易失性读取之后,线程可以看到对F1进行易失性写入之前的所有内存修改,这与说所有*内存已同步非常不同。它不像运行同步块的任何线程那么简单。
布鲁诺·里斯

1
任何存储器屏障杂交(用synchronizedvolatile)存在用于关系的“之前发生”所有存储器。除非您在同一台监视器上锁定@BrunoReis,否则无法保证锁定和同步的顺序。但是,如果println(...)完成,则可以保证该keepRunning字段已更新。
2013年

3

当一个变量是时volatile,它保证不会被缓存,并且不同的线程将看到更新后的值。但是,不标记它volatile并不能保证相反的效果。volatile这是很长时间以来在JVM中中断的事情之一,但仍然不是很容易理解。


在现代的多处理器@Jeff中,您的最后评论有些错误/令人误解。JVM对于刷新值确实很聪明,因为这样做会降低性能。
2013年

当keepRunning由main设置为false时,线程仍会看到更新,因为JVM聪明地刷新了该值。不过,这不能保证(请参见上面@Gray的评论)。
Jeff Storey

2

volatile不一定会产生巨大的变化,具体取决于JVM和编译器。但是,对于许多(边缘)情况,这可能是优化之间的区别,这与导致变量更改没有被正确写入相比,导致变量的更改无法被注意到。

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

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


2

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

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

查看更多Java易失性示例


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以获得更清晰的说明。


尽管此链接可以回答问题,但最好在此处包括答案的基本部分,并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会无效。
admdrew

是的,您绝对正确。我将其添加。感谢您的宝贵意见。
Azhaguvel 2014年

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尝试该示例。


0

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

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

volatile变量必须仅在线程上下文中使用。看到这里的例子

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.