为什么Volatile表现得很奇怪

时间:2012-07-01 10:49:26

标签: java multithreading volatile synchronized

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

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

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

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

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

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

    3. 但事实并非如此,使用volatile关键字我一直没有得到20000,而是18534,15000等......有时候是20000。

      < / LI>
    4. 但是虽然我使用了synchronized关键字,但它工作得很好,为什么...... ??

    5. 任何人都可以向我解释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);
          }
      }
      

4 个答案:

答案 0 :(得分:12)

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

  1. 读取变量的当前值
  2. 增加值
  3. 将新值写回变量
  4. 这三个步骤交织在一起,导致执行路径不同,导致值不正确。如果您想避免使用synchronized关键字,请使用AtomicInteger.incrementAndGet()

    尽管volatile关键字的行为与您所描述的非常相似,但它仅适用于每个单独的操作,而不适用于所有三个操作。

答案 1 :(得分:7)

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

假设有两个线程需要递增计数器,现在设置为5.两个线程都看到5,从中取出6,并将其写回计数器。如果计数器不是volatile,则两个线程都可以假设它们知道值为6,并跳过下一个读取。然而,它是不稳定的,所以它们都会读回6,并继续递增。由于线程没有进入锁定步骤,​​您可能会在输出中看到与10000不同的值,但实际上您几乎没有机会看到20000。

答案 2 :(得分:4)

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

count = count + 1;

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

现在,当您添加syncronized时,SynVsVol.Go会以原子方式执行。也就是说,增量是由一个线程整体完成的,另一个不能修改count直到完成。

最后,缓存仅在同步块中修改的成员变量要容易得多。获取监视器时,编译器可以读取它们的值,将其缓存在寄存器中,对该寄存器进行所有更改,并在释放监视器时最终将其刷回主存储器。当您在同步块中调用wait时,以及当您使用其他线程notify时,也会出现这种情况:缓存的成员变量将被同步,并且您的程序将保持一致。 That's guaranteed即使成员变量未声明为volatile:

  

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

答案 3 :(得分:3)

您的代码被破坏了,因为它将volatile上的读取和递增操作视为原子,而不是。代码不包含数据竞争,但它在int上包含竞争条件