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

2022-08-31 09:56:05

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

为了更深入地了解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();
    }
}

答案 1

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

您可以提出应该设计和记录为专门执行原子增量的参数,以便使用 执行非原子增量。然而,这将破坏Java与C和C++之间的“文化兼容性”。同样,它将带走熟悉类C语言的程序员认为理所当然的方便符号,赋予它仅在有限情况下适用的特殊含义。i++i = i + 1

基本的C或C++代码会翻译成Java作为;因为使用原子是不合适的。更糟糕的是,从C或其他类似C语言到Java的程序员无论如何都会使用,从而导致不必要的原子指令使用。for (i = 0; i < LIMIT; i++)for (i = 0; i < LIMIT; i = i + 1)i++i++

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

在某些指令集架构中,没有原子或根本没有原子;要在MIPS上做一个原子inc,你必须编写一个软件循环,它使用和:加载链接和存储条件。Load-linked 读取该单词,如果该单词未更改,则存储条件存储新值,否则它将失败(检测到并导致重试)。incincllsc


答案 2

i++涉及两个操作:

  1. 读取的当前值i
  2. 递增该值并将其赋值给i

当两个线程同时对同一变量执行时,它们可能都获得相同的当前值 ,然后递增并将其设置为 ,因此您将获得单个增量而不是两个。i++ii+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

推荐