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

And*_*302 94 java concurrency multithreading

为什么i++Java中没有原子?

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

所以我用了一个

private static int total = 0;
Run Code Online (Sandbox Code Playgroud)

在主要班级.

我有两个主题.

  • 线程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();
    }
}
Run Code Online (Sandbox Code Playgroud)

Kaz*_*Kaz 119

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

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

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

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

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

  • 问题的第一个字是"为什么".截至目前,这是解决"为什么"问题的唯一答案.其他答案真的只是重新陈述了这个问题.所以+1. (22认同)
  • 值得注意的是,原子性保证不会解决非易失性字段更新的可见性问题.因此,一旦一个线程使用了`++`运算符,除非你将每个字段视为隐式"volatile",否则这样的原子性保证将无法解决并发更新问题.那么,如果不解决问题,为什么可能会浪费性能. (3认同)
  • 因为java没有指针,增加局部变量本身就是线程保存,所以对于循环,问题大多不会那么糟糕.当然,你的观点是最少的惊喜.同样,`i = i + 1`将是`++ i`的翻译,而不是`i ++` (2认同)

Era*_*ran 33

i++ 涉及两个操作:

  1. 读取当前值 i
  2. 递增值并将其分配给 i

当两个线程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
Run Code Online (Sandbox Code Playgroud)

  • +1,但"1. A,2.B和C"听起来像三个操作,而不是两个.:) (14认同)
  • 但我认为问题是"为什么"而不是"会发生什么". (6认同)
  • 请注意,即使操作是使用单个机器指令实现的,该指令使存储位置增加,也不能保证它是线程安全的.机器仍然需要获取值,增加值并将其存储回来,*plus*可能存在该存储位置的多个缓存副本. (3认同)
  • @Aquarelle - 如果两个处理器同时对同一个存储位置执行相同的操作,并且该位置没有"预留"广播,那么它们几乎肯定会干扰并产生虚假结果.是的,此操作可能是"安全的",但即使在硬件级别也需要特别的努力. (3认同)

Jon*_*nne 11

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


kst*_*tis 6

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

  1. 读取当前值
  2. 写入新值
  3. 存储新值

这三个操作并不意味着在单个步骤中执行,或者换句话说i++不是 复合操作。因此,当多个线程参与一个单一但非复合的操作时,各种事情都可能出错。

考虑以下场景:

时间 1 :

Thread A fetches i
Thread B fetches i
Run Code Online (Sandbox Code Playgroud)

时间 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)
Run Code Online (Sandbox Code Playgroud)

时间 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.
Run Code Online (Sandbox Code Playgroud)

你有它。竞争条件。


这就是为什么i++不是原子的。如果是这样,这一切都fetch-update-store不会发生,并且每个都会原子地发生。这正是AtomicInteger它的用途,在您的情况下,它可能适合。

聚苯乙烯

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

  • 唔。语言可以将任何功能定义为原子功能,无论是增量还是独角兽。你只是举例说明了非原子性的结果。 (2认同)
  • 虽然我明白你的意思,但你的回答对学习来说有点令人困惑。我看到一个例子,结论是“因为例子中的情况”;恕我直言,这是一个不完整的推理:( (2认同)

Ani*_*kur 5

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

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

线程1和2:

  1. 从内存中获取总值
  2. 将1添加到值
  3. 写回内存

如果没有同步,那么让我们说线程1读取值3并将其增加到4,但是没有写回来.此时,发生上下文切换.线程2读取值3,递增它并发生上下文切换.虽然两个线程都增加了总值,但仍然是4 - 竞争条件.

  • @josefx:请注意,我不是在质疑事实,而是在这个答案中的推理.它基本上说_"我的Java在Java中不是原子的,因为它具有竞争条件"_,这就像说_"一辆汽车没有安全气囊,因为碰撞可能发生"_或_"你没有刀currywurst-order因为香肠可能需要切割"_.因此,我不认为这是一个答案.问题不是_"我做了什么?"_或_"i ++未被同步的后果是什么?"_. (4认同)
  • 我不知道这应该如何回答这个问题.语言可以将任何特征定义为原子,无论是增量还是独角兽.你只是举例说明了不是原子的结果. (2认同)
  • (抱歉我在第一条评论中的严厉语气)但是,原因似乎是“因为如果它是原子的,那么就不会有竞争条件”。即,听起来似乎需要竞争条件。 (2认同)