【问题标题】:Why Volatile is behaving weirdly为什么 Volatile 行为怪异
【发布时间】:2012-07-02 03:19:36
【问题描述】:

我最近遇到了 volatile 关键字的这种奇怪行为。据我所知,

  1. volatile 关键字应用于变量以反映对数据所做的更改 变量由一个线程转移到另一个线程。

  2. volatile 关键字防止在线程上缓存数据。

我做了一个小测试............

  1. 我使用了一个名为count的整数变量,并在其上使用了volatile关键字。

  2. 然后做了2个不同的线程将变量值增加到10000,所以最终结果应该是20000。

  3. 但情况并非总是如此,使用 volatile 关键字我不会始终获得 20000,而是 18534、15000 等......有时甚至是 20000。

  4. 但是当我使用同步关键字时,它工作得很好,为什么....??

谁能解释一下 volatile 关键字的这种行为。

我正在发布我的带有 volatile 关键字的代码以及带有 synchronzied 关键字的代码。

以下代码在变量计数上与 volatile 关键字的行为不一致

public class SynVsVol implements Runnable{

    volatile int  count = 0;

    public void go(){

        for (int i=0 ; i<10000 ; i++){
             count = count + 1;
         }
    }

    @Override
    public void run() {
        go();
    }

    public static void main(String[] args){

        SynVsVol s = new SynVsVol();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("Total Count Value: "+s.count);
    }
}

以下代码与 go() 方法中的 synchronized 关键字完美结合。

public class SynVsVol implements Runnable{

    int  count = 0;

    public synchronized void go(){
        for (int i=0 ; i<10000 ; i++){
             count = count + 1;
         }
    }

    @Override
    public void run() {
        go();
    }

    public static void main(String[] args){

        SynVsVol s = new SynVsVol();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("Total Count Value: "+s.count);
    }
}

【问题讨论】:

  • 想象一个线程在count = count + 1 指令期间被切换——特别是在它接收count 之后和存储count + 1 之前。你只需要看到这种情况发生几千次,你就完成了。使用AtomicInteger 解决此问题。

标签: java multithreading volatile synchronized


【解决方案1】:

count = count + 1 不是原子的。它分为三个步骤:

  1. 读取变量的当前值
  2. 增加值
  3. 将新值写回变量

这三个步骤交织在一起,导致执行路径不同,导致值不正确。如果您想避免使用 synchronized 关键字,请改用 AtomicInteger.incrementAndGet()

因此,尽管volatile 关键字的作用与您描述的差不多,但这仅适用于每个单独的操作,而不适用于所有三个操作。

【讨论】:

  • 是的。据我了解,volatile 意味着原子读写,但不是例如比较和设置。
  • @Louis 实际上,JVM 保证了指针和原语的原子读取/写入(与可见性保证不同!)直到 int 大小。原子性的额外保证仅对双/长是必需的
  • 对不起,是的。原子性和可见性。
【解决方案2】:

volatile 关键字不是同步原语。它只是防止在线程上缓存该值,但它不会防止两个线程同时修改相同的值并将其写回。

假设有两个线程需要增加计数器的值,计数器现在设置为 5。两个线程都看到 5,从中取出 6,然后将其写回计数器。如果计数器不是volatile,则两个线程都可以假定它们知道值为 6,并跳过下一次读取。然而,它是易变的,所以他们都会读回 6,并继续递增。由于线程没有步调一致,您可能会在输出中看到与 10000 不同的值,但您几乎不可能看到 20000。

【讨论】:

  • volatile 关键字不是同步原语。但它通常用于同步目的。
  • 我认为这不是真的。 volatile 就好像它被封闭在一个同步块中,在自身上同步。
  • 它通常用于与硬件通信,(或者至少,这是我使用它的目的),当编译器无法知道“&UxRBR”位置可以“神奇地”具有新值 pop在没有被另一个软件指令明确写入的情况下进入它。我甚至可以通过将所有此类声明移动到单独的文件中来避免这种使用,以便编译器无论如何都无法优化访问。正如 DBL 所说,即使对于中断处理程序,使用 'volatile' 并不意味着我不必注意递增环形缓冲区指针的顺序。
  • @Getah,好吧,根据the page I linked to in my answer"Access to the variable acts as though it is enclosed in a synchronized block, synchronized on itself."。所以它确实同步,但只有一个操作;不是递增变量所需的三个操作(读取、递增、写入)。
  • volatile 仅提供可见性保证,但不进行任何同步。根据实际 CPU,我们很可能同时进行数千次读取而不会出现问题。
【解决方案3】:

变量是volatile 的事实并不意味着它所涉及的每个操作都是原子的。比如SynVsVol.Go中的这一行:

count = count + 1;

将首先读取count,然后递增,然后将结果写回。如果其他线程同时执行,结果取决于命令的交错。

现在,当您添加 syncronized 时,SynVsVol.Go 会自动执行。即增量由一个线程整体完成,另一个线程在完成之前不能修改count

最后,仅在同步块中修改的成员变量的缓存要容易得多。编译器可以在获取监视器时读取它们的值,将其缓存在寄存器中,在该寄存器上完成所有更改,并最终在监视器释放时将其刷新回主内存。当您在同步块中调用wait 时也是这种情况,并且当某个其他线程notifys 您:缓存的成员变量将被同步时,您的程序将保持连贯性。 That's guaranteed即使成员变量没有声明为volatile:

同步确保线程在之前或之前写入内存 在同步块期间以可预测的方式显示 到在同一监视器上同步的其他线程。

【讨论】:

    【解决方案4】:

    您的代码已损坏,因为它将volatile 上的读取和递增操作视为原子操作,但事实并非如此。该代码不包含数据竞争,但它确实包含int 上的竞争条件

    【讨论】:

      猜你喜欢
      • 2021-08-29
      • 1970-01-01
      • 2014-03-03
      • 2016-10-07
      • 2022-10-15
      • 1970-01-01
      • 2011-12-21
      • 2012-11-21
      • 2019-11-03
      相关资源
      最近更新 更多