我知道i++
等复合操作不是线程安全的,因为它们涉及多个操作。
但是检查引用本身是一个线程安全操作吗?
a != a //is this thread-safe
我尝试对此进行编程并使用多个线程,但它没有失败。我想我无法在我的机器上模拟比赛。
public class TestThreadSafety {
private Object a = new Object();
public static void main(String[] args) {
final TestThreadSafety instance = new TestThreadSafety();
Thread testingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
long countOfIterations = 0L;
while(true){
boolean flag = instance.a != instance.a;
if(flag)
System.out.println(countOfIterations + ":" + flag);
countOfIterations++;
}
}
});
Thread updatingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
instance.a = new Object();
}
}
});
testingReferenceThread.start();
updatingReferenceThread.start();
}
}
这是我用来测试线程安全性的程序。
当我的程序在一些迭代之间开始时,我得到输出标志值,这意味着引用!=
检查在同一个引用上失败。但是在一些迭代之后,输出变为常量值false
,然后长时间执行程序不会生成单个true
输出。
正如输出建议在一些n(非固定)迭代之后,输出似乎是常数值并且不会改变。
输出:
对于某些迭代:
1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true
答案 0 :(得分:122)
在没有同步的情况下使用此代码
Object a;
public boolean test() {
return a != a;
}
可能会产生true
。这是test()
ALOAD 0
GETFIELD test/Test1.a : Ljava/lang/Object;
ALOAD 0
GETFIELD test/Test1.a : Ljava/lang/Object;
IF_ACMPEQ L1
...
我们可以看到它将字段a
加载到本地变量两次,这是一个非原子操作,如果a
之间被另一个线程比较更改可能会产生false
。
此外,内存可见性问题与此相关,无法保证当前线程可以看到另一个线程对a
所做的更改。
答案 1 :(得分:47)
支票
a != a
线程安全吗?
如果a
可能被另一个线程更新(没有正确的同步!),那么No。
我尝试对此进行编程并使用多个线程,但没有失败。我想无法在我的机器上模拟比赛。
这并不意味着什么!问题是,如果JLS允许允许允许其他线程更新a
的执行,则代码不是线程安全的。您不能在特定计算机和特定Java实现上使用特定测试用例导致竞争条件发生的事实并不排除在其他情况下发生这种情况。
这是否意味着!= a可以返回
true
。
是的,理论上,在某些情况下。
或者,即使a != a
同时发生变化,false
也可能会返回a
。
关于“奇怪的行为”:
当我的程序在一些迭代之间开始时,我得到输出标志值,这意味着引用!=检查在同一个引用上失败。但是经过一些迭代后,输出变为常量值false,然后长时间执行程序不会产生单个真输出。
这种“怪异”行为与以下执行方案一致:
加载程序,JVM启动解释字节码。由于(正如我们从javap输出中看到的)字节码执行两次加载,您(显然)偶尔会看到竞争条件的结果。
一段时间后,代码由JIT编译器编译。 JIT优化器注意到同一内存插槽(a
)的两个负载靠近在一起,并优化第二个负载。 (事实上,它有可能完全优化测试......)
现在比赛条件不再显现,因为不再有两次加载。
请注意, all 与JLS允许Java实现的内容一致。
@kriss评论如此:
这看起来可能是C或C ++程序员所谓的“未定义行为”(依赖于实现)。好像在像这样的角落里的Java中可能会有一些UB。
Java内存模型(在JLS 17.4中指定)指定了一组前提条件,在这些前提条件下,一个线程可以保证看到另一个线程写入的内存值。如果一个线程试图读取另一个线程写入的变量,并且不满足这些前提条件,那么可能存在许多可能的执行......其中一些可能是不正确的(从应用程序的要求的角度来看)。换句话说,定义了可能行为的集(即“格式良好的执行”集合),但我们不能说这些行为中的哪一个会发生。
如果代码的最终效果相同,则允许编译器组合并重新排序加载并保存(并执行其他操作):
但是如果代码没有正确同步(因此“之前发生”关系没有充分约束一组格式正确的执行),编译器可以以“不正确”的方式重新排序加载和存储“结果。 (但这只是说程序不正确。)
答案 2 :(得分:27)
用test-ng证明:
public class MyTest {
private static Integer count=1;
@Test(threadPoolSize = 1000, invocationCount=10000)
public void test(){
count = new Integer(new Random().nextInt());
Assert.assertFalse(count != count);
}
}
我有2次失败的10 000次调用。所以否,不线程安全
答案 3 :(得分:15)
不,不是。对于比较,Java VM必须将两个值放在堆栈上进行比较并运行compare指令(哪一个取决于“a”的类型)。
Java VM可以:
false
在第一种情况下,另一个线程可以修改两次读取之间“a”的值。
选择哪种策略取决于Java编译器和Java Runtime(尤其是JIT编译器)。它甚至可能在程序运行期间发生变化。
如果要确定如何访问变量,必须将其设为volatile
(所谓的“半内存屏障”)或添加完整的内存屏障(synchronized
)。您还可以使用一些高级API(例如,Juned Ahasan提到的AtomicInteger
)。
有关线程安全的详细信息,请阅读JSR 133(Java Memory Model)。
答案 4 :(得分:6)
Stephen C.已经很好地解释了这一点。有趣的是,您可以尝试使用以下JVM参数运行相同的代码:
-XX:InlineSmallCode=0
这应该阻止JIT完成优化(它在hotspot 7服务器上完成),你将永远看到true
(我停在2,000,000,但我想它会在此之后继续)。
有关信息,以下是JIT代码。说实话,我不会流利地阅读装配,以了解测试是否实际完成或两个负载来自何处。 (第26行是测试flag = a != a
,第31行是while(true)
的右大括号。)
# {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1'
0x00000000027dcc80: int3
0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0]
0x00000000027dcc8c: data32 data32 xchg ax,ax
0x00000000027dcc90: mov DWORD PTR [rsp-0x6000],eax
0x00000000027dcc97: push rbp
0x00000000027dcc98: sub rsp,0x40
0x00000000027dcc9c: mov rbx,QWORD PTR [rdx+0x8]
0x00000000027dcca0: mov rbp,QWORD PTR [rdx+0x18]
0x00000000027dcca4: mov rcx,rdx
0x00000000027dcca7: movabs r10,0x6e1a7680
0x00000000027dccb1: call r10
0x00000000027dccb4: test rbp,rbp
0x00000000027dccb7: je 0x00000000027dccdd
0x00000000027dccb9: mov r10d,DWORD PTR [rbp+0x8]
0x00000000027dccbd: cmp r10d,0xefc158f4 ; {oop('javaapplication27/TestThreadSafety$1')}
0x00000000027dccc4: jne 0x00000000027dccf1
0x00000000027dccc6: test rbp,rbp
0x00000000027dccc9: je 0x00000000027dcce1
0x00000000027dcccb: cmp r12d,DWORD PTR [rbp+0xc]
0x00000000027dcccf: je 0x00000000027dcce1 ;*goto
; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
0x00000000027dccd1: add rbx,0x1 ; OopMap{rbp=Oop off=85}
;*goto
; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
0x00000000027dccd5: test DWORD PTR [rip+0xfffffffffdb53325],eax # 0x0000000000330000
;*goto
; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
; {poll}
0x00000000027dccdb: jmp 0x00000000027dccd1
0x00000000027dccdd: xor ebp,ebp
0x00000000027dccdf: jmp 0x00000000027dccc6
0x00000000027dcce1: mov edx,0xffffff86
0x00000000027dcce6: mov QWORD PTR [rsp+0x20],rbx
0x00000000027dcceb: call 0x00000000027a90a0 ; OopMap{rbp=Oop off=112}
;*aload_0
; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
; {runtime_call}
0x00000000027dccf0: int3
0x00000000027dccf1: mov edx,0xffffffad
0x00000000027dccf6: mov QWORD PTR [rsp+0x20],rbx
0x00000000027dccfb: call 0x00000000027a90a0 ; OopMap{rbp=Oop off=128}
;*aload_0
; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
; {runtime_call}
0x00000000027dcd00: int3 ;*aload_0
; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
0x00000000027dcd01: int3
答案 5 :(得分:5)
不,a != a
不是线程安全的。此表达式由三部分组成:加载a
,再次加载a
,然后执行!=
。另一个线程可以获得a
父级的内部锁定,并在两次加载操作之间更改a
的值。
另一个因素是a
是否是本地的。如果a
是本地的,那么没有其他线程可以访问它,因此应该是线程安全的。
void method () {
int a = 0;
System.out.println(a != a);
}
也应始终打印false
。
将a
声明为volatile
无法解决问题a
是static
还是实例。问题不在于线程具有不同的a
值,而是一个线程使用不同的值加载a
两次。实际上它可能会使案例更不是线程安全的。如果a
不是volatile
,那么a
可能会被缓存,而另一个线程的更改不会影响缓存的值。< / p>
答案 6 :(得分:3)
关于奇怪的行为:
由于变量a
未标记为volatile
,因此在某些时候,线程可能会缓存a
的值。 a
a != a
的{{1}}都是缓存版本,因此始终相同(意味着flag
现在始终是false
)。
答案 7 :(得分:0)
即使是简单的阅读也不是原子的。如果a
为long
且未标记为volatile
,那么在32位JVM上long b = a
不是线程安全的。