为什么在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 ();
}
} |
- 你为什么不试试AtomicInteger?
- 另一个问题,在Java中不考虑原子,并发。
- @用户2864740,为什么你说原子整数不是原子?您可以使用getandincrement方法来执行此操作。它是原子的。
- jvm有一个用于增加整数的iinc操作,但这只适用于局部变量,在这种情况下,并发性不受关注。对于字段,编译器分别生成读-修改-写命令。
- 为什么你甚至期望它是原子的?
- 即使在实现"增量存储位置"指令的硬件上,也不能保证这是线程安全的。仅仅因为一个操作可以表示为一个单独的操作符,就不能说明它的线程安全性。
- @愚蠢的怪胎:即使有一条针对字段的iinc指令,有一条指令也不能保证原子性,例如非volatilelong和double字段访问不能保证是原子的,不管它是由一个字节码指令执行的。
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,必须编写一个使用ll和sc的软件循环:加载链接,并有条件地存储。加载链接读取单词,如果单词没有更改,则存储条件存储新值,否则将失败(检测到并导致重试)。
- 由于Java没有指针,所以递增局部变量本质上是线程保存,所以使用循环,问题大多不会那么糟糕。当然,你关于最不令人惊讶的观点。同样,正如它所说,i = i + 1是++i的翻译,而不是i++的翻译。
- 问题的第一个词是"为什么"。到目前为止,这是解决"为什么"问题的唯一答案。其他答案只是重新陈述问题。那么+1。
- 值得注意的是,原子性保证不能解决非volatile字段更新的可见性问题。因此,除非一个线程在每个字段上使用了++操作符,否则这样的原子性保证不会解决并发更新问题,除非您将每个字段隐式地视为volatile。所以,如果某件事情不能解决问题,为什么还要浪费性能呢?
- @你不是说埃多克斯1〔9〕吗?;)
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 |
- (即使i++是原子的,它也不会是定义良好的/线程安全的行为。)
- "1","1"。A,2。B和C"听起来像是三个操作,而不是两个。:)
- 请注意,即使该操作是用单个机器指令实现的,该指令在适当位置增加了存储位置,也不能保证它是线程安全的。机器仍然需要获取该值、增加该值并将其存储回去,另外该存储位置可能有多个缓存副本。
- @热舔不一定。如果取数、增量和存储是以原子方式完成的,或者在一个"事务"(在CPU级别)中完成的,那么它可能是线程安全的。虽然,是的,可能有缓存副本,但情况不同。我认为唯一可能的争用是在增量操作完成之前暂停增量线程(由于内核调度),可能允许另一个线程读取变量。我认为这取决于线程实现、操作系统和CPU。
- @Aquarelle—如果两个处理器同时对同一存储位置执行相同的操作,并且该位置上没有"保留"广播,那么它们几乎肯定会干扰并产生虚假结果。是的,这个操作可能是"安全的",但它需要特别的努力,即使在硬件级别。
- 但我认为问题是"为什么",而不是"发生了什么"。
- @菲利内尔,我对这个问题的理解不同,因为如果OP想知道为什么Java创作者选择不做++原子,那么就不需要发布所有的代码。代码暗示他们问为什么代码的行为是这样的。根据公认的答案,我可能错了。
- 谢谢,不过,我认为1)读取当前值;2)递增值;3)重新分配仍然是三个操作:)
重要的是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-争用条件。
- 我不知道这应该怎么回答这个问题。一种语言可以将任何特性定义为原子特性,无论是增量特性还是独角兽特性。你只是举了一个不原子的例子。
- 是的,语言可以将任何特征定义为原子,但就Java而言,增量运算符(这是OP发布的问题)不是原子的,我的答案说明了原因。
- (对我在第一次评论中的严厉语气表示抱歉)但是,原因似乎是"因为如果它是原子的,那么就没有种族条件"。也就是说,这听起来似乎是一个种族条件是可取的。
- @phresnel保持一个增量原子的开销是巨大的,很少需要,保持操作便宜,因此大部分时间都需要非原子的开销。
- @注意,我不是在质疑事实,而是在这个答案中的推理。它基本上说"i++在Java中不是原子的,因为它有种族条件",这就像说"一辆车没有安全气囊,因为可能发生的碰撞"或者"你不用刀,因为WurST可能需要被切断"。因此,我不认为这是一个答案。问题不是"我+做什么?"或者"I++不同步的后果是什么?".
i++是一个只涉及3个操作的语句:
读取当前值
写新价值
存储新的价值
这三个操作不是一步执行的,换句话说,i++不是一个复合操作。因此,当一个非复合操作涉及多个线程时,所有种类的事情都可能出错。
举个例子,想象一下这个场景:
时间1:
时间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++不是原子操作。
- 当我理解你的观点时,你的回答对学习有点困惑。我看到一个例子,一个结论说"因为例子中的情况";imho这是一个不完整的推理:(
- @phresnel也许不是最具教育意义的答案,但它是我目前能提供的最好的答案。希望它能帮助人们而不是混淆他们。不过,谢谢你的批评。我会在以后的帖子中尽量更准确。
有两个步骤:
从记忆中提取我
设置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和其他原子类。
- 这是误导。必须分离执行并获取结果,否则将无法从任何原子操作中获取值。
- 不,它不是,这就是为什么Java的AtomicInteger有一个GET()、GETAND递增()、GETAND减量()、增量和()、减量和GET()等。
- Java语言可以定义EDCOX1,6,扩展到EDCOX1,12。这种扩张并不是什么新鲜事。例如,C++中的lambda被扩展到C++中的匿名类定义。
- 给定一个原子i++,人们可以轻而易举地创造一个原子++i,反之亦然。一个等于另一个加一个。
在JVM中,增量涉及读和写,因此它不是原子的。
并发(EDCOX1,6)类是Java V1.0中的附加特性。在此之前,在beta中添加了i++,因此,它在最初的实现中(或多或少)仍然是很有可能的。
由程序员来同步变量。查看Oracle的教程。
编辑:为了澄清,i++是一个定义良好的过程,它早于Java,因此Java设计者决定保留该过程的原始功能。
++操作符是在B(1969)中定义的,它比Java和线程只需一个TAD。
- -1"公共类线程…从:jdk1.0"来源:docs.oracle.com/javase/7/docs/api/index.html?Java/Lang//Helip;
- 这个版本并不重要,因为它仍然在线程类之前实现,并且没有因为线程类而更改,但是我已经编辑了我的答案以取悦您。
- 重要的是,您的声明"它仍然在线程类之前实现"不受源支持。i++不是原子的是一个设计决策,而不是一个不断增长的系统中的监督。
- 哈哈,真可爱。i++在线程之前定义得很好,仅仅因为在Java之前有语言存在。Java的创建者使用那些其他语言作为基础,而不是重新定义一个被接受的过程。我在哪里说过是疏忽?
- @Sillyfreak这里有一些资料显示了++有多古老:en.wikipedia.org/wiki/increment_and_decrement_operators en.wikipedia.org/wiki/b_uu(编程语言)
- 仅仅因为一个语言特性是从其他语言中的一个对应特性中借来的,或者受到其他语言中相应特性的启发,并不意味着它必须保持完全相同的基本特性。例如,考虑不同语言的lambda函数的多样性(例如,与真正的lisp lambdas symbo1ics.com/blog/相比,python的lambda函数的弱点)。p=1292)。即使在C++中,从C中获取的某些特征与C对应的特征也不尽相同,尽管C++经常被认为与C(CPLACO.COM/TURBAC/C-VS+C++.html)是向后兼容的。
- @但是许多借用的关键字和运算符并不像它们借用的语言那样实现。例如,C++中的EDOCX1 2和EDOCX1 3完全与Java的"等价物"正交。