繁体   English   中英

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

[英]Simplest and understandable example of volatile keyword in Java

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

我有两个基本问题:

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

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

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

易失性不能替代同步

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

例:

volatile int i = 0;

public void incrementI(){
   i++;
}

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

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

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

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

也:

可以使用同步块代替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变量:

  • 写入变量不取决于其当前值,或者您可以确保只有一个线程更新该值;
  • 该变量不与其他状态变量一起参与不变式。
  • 在访问变量时,由于其他任何原因都不需要锁定。

调试技巧 :调用JVM时,即使用于开发和测试,也请务必始终指定-server JVM命令行开关。 服务器JVM比客户端JVM执行更多的优化,例如从循环中提升未在循环中修改的变量。 在开发环境(客户端JVM)中似乎有效的代码可能会在部署环境(服务器JVM)中中断。

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

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

什么是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您的变量不会被缓存,即可以在all machinesrun fine

private volatile boolean running = true;  //volatile keyword

因此,使用volatile关键字是一种good safer programming practice

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

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

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

无论是println(...)行是输入还是输出,在使用Intel i7的MacBook Pro上,您的程序都会在Java6下为我旋转。

谁能用例子解释易失性? 不符合JLS的理论。

我认为您的榜样很好。 不知道为什么它不能与所有System.out.println(...)语句一起删除。 这个对我有用。

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

在内存同步方面, volatile抛出与synchronized块相同的存储屏障,除了volatile屏障是单向的还是双向的。 volatile读引发负载障碍,而写则引发存储障碍。 synchronized块是带有互斥锁的双向屏障。

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

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

Access_Modifier volatile DataType Variable_Name;

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

在此处输入图片说明

如果线程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();
        }
    }
}

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

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

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

@看到

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

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

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

volatile确保不会发生这种优化,并且所有读写操作都直接在堆或所有线程都可以看到它的其他位置进行。

请在下面找到解决方案,

此变量的值永远不会在线程本地缓存:所有读取和写入操作都将直接进入“主内存”。 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以获得更清晰的说明。

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

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

    }
}

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

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

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

有很多很棒的例子,但我只是想补充一点,在很多情况下需要使用volatile ,因此没有一个具体的例子可以将它们排除在外。

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

查看更多Java易失性示例

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM