为什么i++
在Java中不是原子的?
为了更深入地了解Java,我试着计算线程循环执行的频率。
所以我使用了
private static int total = 0;
在主要班级。
我有两个主题。
System.out.println("Hello from Thread 1!");
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();
}
}
答案 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,您必须编写一个使用ll
和sc
:加载链接和存储条件的软件循环。加载链接读取单词,如果单词未更改,则store-conditional存储新值,否则失败(检测到并导致重新尝试)。
答案 1 :(得分:32)
i++
涉及两项操作:
i
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
答案 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:
如果没有同步,那么让我们说第一个线程已读取值3并将其增加到4,但尚未将其写回。此时,发生上下文切换。线程2读取值3,递增它并发生上下文切换。虽然两个线程都增加了总值,但它仍然是4 - 竞争条件。
答案 4 :(得分:4)
i++
是一个只涉及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
的用途,在你的情况下,它可能恰好适合。
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
(因为我们发布增量,例如先读取,然后更新)
这是操作无法原子化的原因之一,因为您需要读取值(并使用它执行某些操作),然后然后更新值。
另一个重要原因是,由于锁定,以原子方式通常做一些事情会花费更多时间。如果人们想要进行原子操作的罕见情况,那么对基元的所有操作都会花费更长的时间,这将是愚蠢的。这就是为什么他们在语言中添加了AtomicInteger
和other原子类。
答案 6 :(得分:2)
有两个步骤:
所以它不是原子操作。 当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和线程只是一点点。