为什么我不是原子++?


97

为什么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();
    }
}

14
您为什么不尝试AtomicInteger
Braj 2014年


3
JVM有一个iinc用于递增整数的操作,但是仅适用于不关心并发性的局部变量。对于字段,编译器将分别生成读取-修改-写入命令。
傻傻的怪物

14
您为什么甚至期望它是原子的?
Hot Licks

2
@Silly Freak:即使有一条iinc针对字段的指令,拥有一条指令也不能保证原子性,例如,非访问volatile longdouble字段访问都不能保证是原子性的,无论它是由一条字节码指令执行的事实。
Holger 2014年

Answers:


125

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中,必须使用特殊的指令“锁前缀”使该inc指令具有原子性:出于与上述相同的原因。如果inc始终是原子的,则在需要非原子的inc时将永远不会使用它;程序员和编译器会生成加载,加1和存储的代码,因为这样做会更快。

在某些指令集体系结构中,没有原子inc或根本没有原子inc。要在MIPS上做一个原子公司,您必须编写一个软件循环,该循环使用lland sc:负载链接和存储条件。链接加载读取单词,如果单词未更改,存储条件存储新值,否则失败(检测到该单词并导致重试)。


2
由于Java没有指针,因此增加局部变量本质上是线程保存,因此使用循环,问题通常不会那么糟糕。当然,关于最少惊喜的观点。照原样,i = i + 1也应该是的翻译++i,而不是i++
Silly Freak

22
问题的第一个词是“为什么”。到目前为止,这是解决“为什么”问题的唯一答案。其他答案实际上只是在重述问题。所以+1。
达伍德·伊本·卡里姆

3
可能值得注意的是,原子性担保不能解决非volatile字段更新的可见性问题。因此,除非volatile一个线程++在其上使用了运算符,否则除非您将每个字段都隐式地对待,否则这种原子性保证将无法解决并发更新问题。因此,如果不能解决问题,为什么可能会浪费性能。
Holger 2014年

1
@DavidWallace不是++吗?;)
Dan Hlavenka 2014年

36

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

(即使i++ 原子的,也不会是定义明确/线程安全的行为。)
user2864740 2014年

15
+1,但“ 1. A,2。B和C”听起来像是三个操作,而不是两个。:)
yshavit 2014年

3
注意,即使使用单个机器指令实现了该操作,该指令增加了适当的存储位置,也无法保证它是线程安全的。机器仍然需要获取值,将其递增并存储回去,再加上该存储位置可能有多个缓存副本。
热门点击

3
@Aquarelle-如果两个处理器同时在相同的存储位置上执行相同的操作,并且该位置上没有“保留”广播,那么它们几乎肯定会干扰并产生虚假结果。是的,此操作有可能是“安全的”,但即使在硬件级别,也需要付出特别的努力。
Hot Licks

6
但是我认为问题是“为什么”,而不是“会发生什么”。
塞巴斯蒂安·马赫

11

重要的是JLS(Java语言规范),而不是JVM的各种实现可能未实现某种语言功能的方式。JLS在第15.14.2节中定义了++后缀运算符,该运算符表示ia“将值1加到变量的值上,然后将总和存储回变量中”。它在任何地方都没有提到或暗示多线程或原子性。对于这些,JLS提供了易失性同步性。此外,还有包java.util.concurrent.atomic(请参阅http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html


5

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

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

线程1和2:

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

如果没有同步,则可以说线程一读取了值3并将其递增为4,但尚未将其写回。此时,上下文切换发生。线程2读取值3,将其递增,然后进行上下文切换。尽管两个线程都增加了合计值,但仍将是4-竞争条件。


2
我不知道这应该如何回答这个问题。语言可以将任何特征定义为原子性,无论是增量还是独角兽。您仅举例说明不是原子的后果。
塞巴斯蒂安·马赫

是的,语言可以将任何功能定义为原子性,但就Java而言,增量运算符(OP提出的问题)不是原子性,我的回答指出了原因。
Aniket Thakur 2014年

1
(对不起,我在第一个评论中说的很刺耳)但是,原因似乎是“因为如果它是原子的,那么就不会有比赛条件”。即,听起来好像是需要比赛条件。
塞巴斯蒂安·马赫2014年

@phresnel为保持增量原子而引入的开销是巨大的,很少需要,因此保持操作便宜,因此大多数时候非原子是可取的。
josefx 2014年

4
@josefx:请注意,我不是在质疑事实,而是在回答这个问题。它基本上说“ i ++在Java中不是因为它具有的竞争条件而原子化的”,这就像在说“由于可能发生的碰撞,汽车没有安全气囊”“因为咖喱香肠的订单您没有刀子,因为可能需要削减香肠”。因此,我认为这不是答案。问题不是“我可以做什么?” “不同步i ++有什么后果?”
塞巴斯蒂安·马赫

5

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

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

这三个操作并不意味着要在单个步骤中执行,换句话说,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目的,在您的情况下,它可能恰好适合。

聚苯乙烯

一本涵盖所有这些问题的出色书籍,然后是其中一本: Java并发实践


1
嗯 语言可以将任何特征定义为原子性,无论是增量还是独角兽。您仅举例说明不是原子的后果。
塞巴斯蒂安·马赫2014年

@phresnel确实如此。但我也指出,这不是一个单一的操作,从广义上讲,它意味着将多个此类操作转换为原子操作的计算成本要高得多,这又(部分地)证明了为什么i++不是原子操作是合理的。
kstratis 2014年

1
当我明白你的意思时,你的答案会使学习有些混乱。我看到一个示例,并得出结论说“由于示例中的情况”;恕我直言,这是一个不完整的推理:(
塞巴斯蒂安·马赫2014年

1
@phresnel也许不是最教学的答案,但这是我目前能提供的最好的答案。希望它能帮助人们,而不会使他们感到困惑。但是,感谢您的批评。在以后的文章中,我将尝试更加精确。
kstratis 2014年

2

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


2

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

例如,看下面的代码:

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

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

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

另一个重要原因是,由于锁定,原子地执行某件事通常会花费更多时间。在人们想要进行原子操作的极少数情况下,将所有对基元的操作花费更长的时间是愚蠢的。这就是为什么他们已经增加AtomicInteger其他原子类的语言。


2
这是误导。您必须分开执行并获得结果,否则您将无法从任何原子操作中获取值。
塞巴斯蒂安·马赫2014年

不,不是,这就是为什么Java的AtomicInteger具有get(),getAndIncrement(),getAndDecrement(),incrementAndGet(),decrementAndGet()等的原因
Roy van Rijn 2014年

1
并且Java语言本来可以定义i++为扩展为i.getAndIncrement()。这样的扩张并不新鲜。例如,C ++中的lambda扩展为C ++中的匿名类定义。
塞巴斯蒂安·马赫

给定一个原子,i++就可以轻松创建一个原子++i,反之亦然。一个等于另一个加一。
David Schwartz 2015年

2

分两个步骤:

  1. 从内存中获取我
  2. 将i + 1设置为i

所以这不是原子操作。当线程1执行i ++,并且线程2执行i ++时,i的最终值可能是i + 1。


-1

并发(Thread类等)是Java v1.0中的新增功能。i++是在Beta版之前添加的,因此,它(或多或少)原始实施中的可能性仍然很大。

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

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

B(1969)中定义了++运算符,该运算符早于java和线程处理,只是出现了一点点。


-1“公共类线程...自:JDK1.0起”来源:docs.oracle.com/javase/7/docs/api/index.html?java/lang/…
Silly Freak

该版本无关紧要,因为它仍然在Thread类之前实现,并且因为它而没有更改,但我已经编辑了答案以取悦您。
TheBat 2014年

5
重要的是您的声明“它仍然在Thread类之前实现”,没有源支持。i++不是原子的是设计决定,而不是不断发展的系统中的监督。
傻怪胎

哈哈,真可爱。i ++是在Threads之前定义好的,这仅仅是因为Java之前存在着一些语言。Java的创建者使用这些其他语言作为基础,而不是重新定义一个公认的过程。我曾经在哪里说这是疏忽?
TheBat 2014年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.