2017-02-23 51 views
2

我正在学习volatile变量。我知道什么是volatile,我为volatile变量编写了一个示例程序,但没有按预期工作。Java volatile关键字按预期工作

为什么“计数”未来一段时间小于2000年的终值我已经使用挥发性因此系统不应该缓存“计数”变量和值应始终是2000

当我用同步方法工作正常,但不是在volatile关键字的情况下。

public class Worker { 

private volatile int count = 0; 
private int limit = 10000; 

public static void main(String[] args) { 
    Worker worker = new Worker(); 
    worker.doWork(); 
} 

public void doWork() { 
    Thread thread1 = new Thread(new Runnable() { 
     public void run() { 
      for (int i = 0; i < limit; i++) { 

        count++; 

      } 
     } 
    }); 
    thread1.start(); 
    Thread thread2 = new Thread(new Runnable() { 
     public void run() { 
      for (int i = 0; i < limit; i++) { 

        count++; 

      } 
     } 
    }); 
    thread2.start(); 

    try { 
     thread1.join(); 
     thread2.join(); 
    } catch (InterruptedException ignored) {} 
    System.out.println("Count is: " + count); 
} 
} 

谢谢您提前!

+1

易失性和同步是不一样的...使用原子代替... –

+2

*“我知道什么易变”*没有冒犯,但这个问题证明,否则。 – Tom

+1

“我使用了volatile,因此系统不应该缓存”count“variable”,除了竞争条件(这里显而易见的问题),这是完全错误的。易失性不会阻止CPU缓存内存中的值 - 甚至没有办法告诉现代CPU做这样的事情,因为它毫无意义。 Volatile做了一些非常不同的事情,如果不了解内存排序和可视性保证是什么(并且在了解它之后你会注意到有更好的高级解决方案)。 – Voo

回答

8

当你做count++,这是一个读,增量,然后写。两个线程可以分别执行读取操作,每个线程执行增量操作,然后每个线程执行写入操作,结果只产生一次增量。虽然您的读取是原子性的,但您的写入是原子性的,没有值被缓存,这是不够的。您需要的不仅仅是这些 - 您需要一个原子读取 - 修改 - 写入操作,并且volatile不提供此操作。

+0

很好的解释! –

2

count++基本上是这样的:

// 1. read/load count 
// 2. increment count 
// 3. store count 
count = count + 1; 

单独的firstthird操作是原子的。他们所有3个together都不是原子的。

1

i++ is not atomic in Java。因此两个线程可以同时读取,两者都计算为+1为相同的数字,并且两者都存储相同的结果。

编译这个使用javac inc.java

public class inc { 
    static int i = 0; 
    public static void main(String[] args) { 
     i++; 
    } 
} 

阅读使用javap -c inc字节码。我修剪下来,只是显示功能main()

public class inc { 
    static int i; 

    public static void main(java.lang.String[]); 
    Code: 
     0: getstatic  #2     // Field i:I 
     3: iconst_1 
     4: iadd 
     5: putstatic  #2     // Field i:I 
     8: return 
} 

我们看到,增量(静态INT)的使用来实现:getstaticiconst_1iaddputstatic

由于这是用四条指令完成的,并且没有锁,所以不会有原子性的期望。另外值得一提的是,即使这是用1条指令完成,我们可能是出于运气(报价从this thread用户“热舔”的评论):

即使在硬件实现的“增量存储位置”指令,但不能保证这是线程安全的。仅仅因为一个操作可以表示为一个单一的操作员,并没有说它是线程安全的。


如果你真的想解决这个问题,你可以使用AtomicInteger,具有原子性的保证:

final AtomicInteger myCoolInt = new AtomicInteger(0); 
myCoolInt.incrementAndGet(1); 
1

当你使用​​方法,它正在为预期的,因为它确保了如果其中一个线程执行该方法,其他调用者线程的执行将暂停,直到当前正在执行的线程退出该方法。在这种情况下,整个读 - 增量 - 写周期是原子的。

tutorial

首先,它是不可能的同一对象来交织上同步方法 两个调用。当一个线程正在为一个对象执行一个同步方法时,所有其他线程将为同一对象块(暂停执行) 调用 同步方法,直到第一个线程完成对象。其次,当一个同步方法退出时,它会自动建立一个 事件之前的关系,以及任何后续调用同一对象的同步方法。这保证了对对象状态的更改 对所有线程均可见。

当您使用volatile(因为它是由其他人解释)这个周期是不是原子作为使用该关键字并不能保证会有对get和增量步骤之间的其他线程的变量没有其他的写在这个线程上。

对于原子计数而不是​​关键字,您可以使用例如一个AtomicInteger

public class Worker { 
    private AtomicInteger count = new AtomicInteger(0); 
    private int limit = 10000; 

    public static void main(String[] args) { 
     Worker worker = new Worker(); 
     worker.doWork(); 
    } 

    public void doWork() { 
     Thread thread1 = new Thread(new Runnable() { 
      public void run() { 
       for (int i = 0; i < limit; i++) 
        count.getAndIncrement(); 
      } 
     }); 
     thread1.start(); 
     Thread thread2 = new Thread(new Runnable() { 
      public void run() { 
       for (int i = 0; i < limit; i++) 
        count.getAndIncrement(); 
      } 
     }); 

     thread2.start(); 

     try { 
      thread1.join(); 
      thread2.join(); 
     } catch (InterruptedException ignored) { 
     } 
     System.out.println("Count is: " + count); 
    } 
} 

这里getAndIncrement()确保原子读增量集周期。

1

内存可见性和原子是多线程中两个不同但常见的问题。当您使用同步关键字时,它通过获取锁确保两者。而volatile只解决内存可见性问题。 Brain Goetz在他的书Concurrency in practice中解释了何时应该使用volatile。

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

那么,在你的情况看看操作计数++不是原子的。