为什么i ++不是原子的?

时间:2014-08-06 18:55:22

标签: java multithreading concurrency

为什么i++在Java中不是原子的?

为了更深入地了解Java,我试着计算线程循环执行的频率。

所以我使用了

private static int total = 0;

在主要班级。

我有两个主题。

  • 主题1:打印System.out.println("Hello from Thread 1!");
  • 主题2:打印System.out.println("Hello from Thread 2!");

我计算线程1和线程2打印的线条。但是线程1的线条+线程2的线条与打印出的线条总数不匹配。

这是我的代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Test {

    private static int total = 0;
    private static int countT1 = 0;
    private static int countT2 = 0;
    private boolean run = true;

    public Test() {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        newCachedThreadPool.execute(t1);
        newCachedThreadPool.execute(t2);
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException ex) {
            Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
        }
        run = false;
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException ex) {
            Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.out.println((countT1 + countT2 + " == " + total));
    }

    private Runnable t1 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total++;
                countT1++;
                System.out.println("Hello #" + countT1 + " from Thread 2! Total hello: " + total);
            }
        }
    };

    private Runnable t2 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total++;
                countT2++;
                System.out.println("Hello #" + countT2 + " from Thread 2! Total hello: " + total);
            }
        }
    };

    public static void main(String[] args) {
        new Test();
    }
}

10 个答案:

答案 0 :(得分:115)

i++在Java中可能不是原子的,因为原子性是一个特殊要求,在i++的大部分用法中都不存在。该要求具有显着的开销:使增量操作成为原子的成本很高;它涉及软件和硬件级别的同步,这些级别不需要以普通增量存在。

您可以将i++应该设计并记录为特定执行原子增量的参数,以便使用i = i + 1执行非原子增量。然而,这将打破文化兼容性" Java,C和C ++之间。同样,它会删除一个方便的符号,熟悉类C语言的程序员认为这是理所当然的,给它一个特殊的含义,只适用于有限的情况。

for (i = 0; i < LIMIT; i++)这样的基本C或C ++代码将for (i = 0; i < LIMIT; i = i + 1)转换为Java;因为使用原子i++是不合适的。更糟糕的是,程序员从C或其他类C语言转到Java会使用i++,导致不必要地使用原子指令。

即使在机器指令集级别,由于性能原因,增量类型操作通常也不是原子的。在x86中,一个特殊指令&#34;锁定前缀&#34;必须用于使inc指令原子化:出于与上述相同的原因。如果inc总是原子的,那么当需要非原子公司时,它永远不会被使用;程序员和编译器会生成加载,添加1和存储的代码,因为它会更快。

在某些指令集架构中,根本没有原子inc或者根本没有inc;要在MIPS上执行atomic inc,您必须编写一个使用llsc:加载链接和存储条件的软件循环。加载链接读取单词,如果单词未更改,则store-conditional存储新值,否则失败(检测到并导致重新尝试)。

答案 1 :(得分:32)

i++涉及两项操作:

  1. 读取i
  2. 的当前值
  3. 增加值并将其分配给i
  4. 当两个线程同时对同一个变量执行i++时,它们可能都获得相同的当前值i,然后递增并将其设置为i+1,所以你会得到一个增量而不是两个。

    示例:

    int i = 5;
    Thread 1 : i++;
               // reads value 5
    Thread 2 : i++;
               // reads value 5
    Thread 1 : // increments i to 6
    Thread 2 : // increments i to 6
               // i == 6 instead of 7
    

答案 2 :(得分:11)

重要的是JLS(Java语言规范),而不是JVM的各种实现如何实现或不实现该语言的某些特性。 JLS在第15.14.2节中定义了++ postfix运算符,其中说i.a. &#34;将值1添加到变量的值中,并将总和存储回变量&#34;。它没有提到或提示多线程或原子性。对于这些,JLS提供 volatile synchronized 。另外,还有包 java.util.concurrent.atomic (参见http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html

答案 3 :(得分:5)

  

为什么i ++在Java中不是原子的?

让我们将增量操作分解为多个语句:

线程1&amp; 2:

  1. 从内存中获取总值
  2. 为值
  3. 添加1
  4. 回写内存
  5. 如果没有同步,那么让我们说第一个线程已读取值3并将其增加到4,但尚未将其写回。此时,发生上下文切换。线程2读取值3,递增它并发生上下文切换。虽然两个线程都增加了总值,但它仍然是4 - 竞争条件。

答案 4 :(得分:4)

i++是一个只涉及3个操作的声明:

  1. 读取当前值
  2. 写新值
  3. 存储新值
  4. 这三个操作并不是一步完成的,换句话说,i++不是复合操作。因此,当单个但非复合操作涉及多个线程时,各种各样的事情都可能出错。

    作为一个例子想象一下这种情况:

    时间1

    Thread A fetches i
    Thread B fetches i
    

    时间2

    Thread A overwrites i with a new value say -foo-
    Thread B overwrites i with a new value say -bar-
    Thread B stores -bar- in i
    
    // At this time thread B seems to be more 'active'. Not only does it overwrite 
    // its local copy of i but also makes it in time to store -bar- back to 
    // 'main' memory (i)
    

    时间3

    Thread A attempts to store -foo- in memory effectively overwriting the -bar- 
    value (in i) which was just stored by thread B in Time 2.
    
    Thread B has nothing to do here. Its work was done by Time 2. However it was 
    all for nothing as -bar- was eventually overwritten by another thread.
    

    你有它。竞争条件。


    这就是i++不是原子的原因。如果是的话,这一切都不会发生,每个fetch-update-store都会以原子方式发生。这正是AtomicInteger的用途,在你的情况下,它可能恰好适合。

    P.S。

    一本涵盖所有这些问题的优秀书籍,然后是一些: Java Concurrency in Practice

答案 5 :(得分:2)

如果操作i++是原子的,你就没有机会从中读取值。这正是您想要使用i++(而不是使用++i)。

例如,请查看以下代码:

public static void main(final String[] args) {
    int i = 0;
    System.out.println(i++);
}

在这种情况下,我们希望输出为:0 (因为我们发布增量,例如先读取,然后更新)

这是操作无法原子化的原因之一,因为您需要读取值(并使用它执行某些操作),然后然后更新值。

另一个重要原因是,由于锁定,以原子方式通常做一些事情会花费更多时间。如果人们想要进行原子操作的罕见情况,那么对基元的所有操作都会花费更长的时间,这将是愚蠢的。这就是为什么他们在语言中添加了AtomicIntegerother原子类。

答案 6 :(得分:2)

有两个步骤:

  1. 从内存中获取我
  2. 将i + 1设为i
  3. 所以它不是原子操作。 当thread1执行i ++,而thread2执行i ++时,i的最终值可能是i + 1.

答案 7 :(得分:1)

在JVM中,增量涉及读取和写入,因此它不是原子的。

答案 8 :(得分:0)

在 JVM 或任何 VM 中,i++ 等效于以下内容:

int temp = i;     // 1. read
i = temp + 1;    // 2. increment the value then 3. write it back

这就是为什么 i++ 是非原子的。

答案 9 :(得分:-1)

并发(Thread类等)是Java v1.0中的附加功能。之前在测试版中添加了i++,因此它仍然可能在其(或多或少)原始实现中。

程序员可以同步变量。查看Oracle's tutorial on this

编辑:为了澄清,i ++是一个定义良好的程序,早于Java,因此Java的设计者决定保留该程序的原始功能。

++运算符是在B(1969)中定义的,它早于java和线程只是一点点。