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

问题描述 投票:63回答:11

我正在阅读Java中的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吗?不是来自JLS的理论。
  • 是不稳定的替代同步?它是否实现了原子性?
java multithreading concurrency volatile
11个回答
45
投票

易失性 - >保证可见性而非原子性

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

易失性不能代替同步

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

例:

volatile int i = 0;

public void incrementI(){
   i++;
}

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

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

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

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

也:

可以使用synchronized块代替volatile,但反之则不然。


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
投票

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

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

volatile变量只能在Thread Context中使用。看到示例here


26
投票

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

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

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

volatile变量的可见性效果超出了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"的摘录,这是你能找到的关于这个主题的最好的书。


13
投票

我稍微修改了你的例子。现在使用keepRunning作为volatile和non volatile成员的示例:

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

10
投票

什么是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 fine上的all machines

private volatile boolean running = true;  //volatile keyword

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


6
投票

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

Access_Modifier volatile DataType Variable_Name;

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

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 Volatile [Instance Field] - 两者都没有被线程缓存

  • 静态字段对所有线程都是通用的,并存储在方法区域中。静态挥发无用。静态字段无法序列化。
  • 易失性主要用于存储在堆区域中的实例变量。 volatile的主要用途是维护所有线程的更新值。实例volatile字段可以是Serialized

@看到


3
投票

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


2
投票

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

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

volatile确保不会发生这种优化,并且所有读取和写入都直接发送到堆或所有线程将看到它的另一个地方。


2
投票

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

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

需要注意的另一件事是System.out.println(...)是同步的,因为底层的PrintStream会同步以阻止重叠输出。所以你在主线程中“免费”获得内存同步。这仍然无法解释为什么读取循环完全看到更新。

无论println(...)线是进出还是出现,你的程序都会在带有Intel i7的MacBook Pro上用Java 6进行旋转。

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

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

是不稳定的替代同步?它是否实现了原子性?

在记忆同步方面,volatile抛出了与synchronized块相同的记忆障碍,除了volatile障碍是单向的与双向的。 volatile读取了一个加载障碍,而写入引发了商店障碍。 synchronized区块是双向屏障。

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


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

© www.soinside.com 2019 - 2024. All rights reserved.