关于java:为什么i ++不是原子的?

Why is i++ not atomic?

为什么在Java中EDCOX1 0不为原子?

为了在Java中得到更深一点,我试图计算线程中循环的执行频率。

所以我用了

1
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的行数与打印出的行总数不匹配。

这是我的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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();
    }
}


EDCOX1〔0〕在Java中可能不是原子的,因为原子性是EDCOX1〔0〕大部分使用中不存在的特殊要求。这一需求有着巨大的开销:使增量操作成为原子操作的成本很高;它涉及到软件和硬件级别的同步,而这些级别不需要以普通增量的形式出现。

您可以提出这样的论点,即i++应该被设计并记录为专门执行原子增量,以便使用i = i + 1执行非原子增量。但是,这会破坏Java和C和C++之间的"文化兼容性"。此外,它还将去掉一个方便的符号,熟悉C语言的程序员认为这是理所当然的,这给了它一个仅在有限情况下适用的特殊含义。

基本的C或C++代码,如EDCOX1×4,将转化为Java作为EDCOX1,5,因为使用原子EDCOX1,0,这将是不合适的。更糟糕的是,程序员从C或其他类似C语言到Java将使用EDCOX1,0,无论如何,导致不必要的使用原子指令。

即使在机器指令集级别,出于性能原因,增量类型操作通常也不是原子操作。在x86中,必须使用一个特殊的指令"lock prefix"使inc指令成为原子:原因与上述相同。如果inc始终是原子的,那么在需要非原子的inc时,它将永远不会被使用;程序员和编译器将生成加载、添加1和存储的代码,因为这样做会更快。

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


i++涉及两种操作:

  • 读取EDOCX1[1]的当前值
  • 增加值并分配给i
  • 当两个线程同时对同一个变量执行i++时,它们都可以得到相同的i当前值,然后递增并设置为i+1,这样就可以得到一个递增而不是两个递增。

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    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


    重要的是JLS(Java语言规范),而不是JVM的各种实现可能或可能没有实现语言的某些特征。jls在第15.14.2条中定义了++postfix操作符,该操作符表示i.a,"值1被添加到变量的值中,和被存储回变量中"。它没有提到或暗示多线程或原子性。对于这些,JLS提供了易失性和同步性。此外,还有java.util.concurrent.atomic包(请参阅http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html)


    Why is i++ not atomic in Java?

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

    线程1和2:

  • 从内存中提取合计值
  • 值加1
  • 写回存储器
  • 如果没有同步,那么假设线程1读取了值3并将其递增到4,但没有将其写回。此时,将发生上下文切换。线程2读取值3,将其递增,然后发生上下文切换。虽然两个线程都增加了总值,但它仍然是4-争用条件。


    i++是一个只涉及3个操作的语句:

  • 读取当前值
  • 写新价值
  • 存储新的价值
  • 这三个操作不是一步执行的,换句话说,i++不是一个复合操作。因此,当一个非复合操作涉及多个线程时,所有种类的事情都可能出错。

    举个例子,想象一下这个场景:

    时间1:

    1
    2
    Thread A fetches i
    Thread B fetches i

    时间2:

    1
    2
    3
    4
    5
    6
    7
    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:

    1
    2
    3
    4
    5
    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的用意,在您的情况下,它可能正好适合您。

    附笔。

    一本很好的书,涵盖了所有这些问题,还有一些是:Java并发编程实战


    有两个步骤:

  • 从记忆中提取我
  • 设置i + 1到i
  • 所以这不是原子操作。当thread1执行i++,thread2执行i++,i的最终值可能是i+1。


    如果操作i++是原子操作,您将没有机会从中读取值。这正是使用i++而不是使用++i所要做的。

    例如,查看以下代码:

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

    在这种情况下,我们预计产量为:0。(因为我们发布增量,例如先读取,然后更新)

    这是操作不能是原子操作的原因之一,因为您需要读取该值(并对其进行一些操作),然后更新该值。

    另一个重要的原因是,原子化地做一些事情通常需要更多的时间,因为锁定。当人们想要进行原子操作时,让所有对原语的操作花费一点时间来处理罕见的情况是愚蠢的。这就是为什么他们在语言中添加了AtomicInteger和其他原子类。


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


    并发(EDCOX1,6)类是Java V1.0中的附加特性。在此之前,在beta中添加了i++,因此,它在最初的实现中(或多或少)仍然是很有可能的。

    由程序员来同步变量。查看Oracle的教程。

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

    ++操作符是在B(1969)中定义的,它比Java和线程只需一个TAD。