Nar*_*hai 140 java multithreading atomic thread-safety race-condition
我知道复合操作i++
不是线程安全的,因为它们涉及多个操作.
但是检查引用本身是一个线程安全的操作?
a != a //is this thread-safe
Run Code Online (Sandbox Code Playgroud)
我尝试编程并使用多个线程,但它没有失败.我想我无法在我的机器上模拟比赛.
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();
}
}
Run Code Online (Sandbox Code Playgroud)
这是我用来测试线程安全性的程序.
当我的程序在一些迭代之间开始时,我得到输出标志值,这意味着引用!=
检查在同一个引用上失败.但是经过一些迭代后,输出变为常量值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
Run Code Online (Sandbox Code Playgroud)
Evg*_*eev 122
在没有同步这段代码的情况下
Object a;
public boolean test() {
return a != a;
}
Run Code Online (Sandbox Code Playgroud)
可能会产生true
.这是字节码test()
ALOAD 0
GETFIELD test/Test1.a : Ljava/lang/Object;
ALOAD 0
GETFIELD test/Test1.a : Ljava/lang/Object;
IF_ACMPEQ L1
...
Run Code Online (Sandbox Code Playgroud)
因为我们可以看到它将字段加载a
到本地变量两次,它是一个非原子操作,如果a
在另一个线程比较之间进行了更改可能会产生false
.
此外,内存可见性问题在这里是相关的,不能保证a
当前线程对另一个线程所做的更改是可见的.
Ste*_*n C 47
检查是否是
a != a
线程安全的?
如果a
可能被另一个线程更新(没有正确的同步!),那么
我尝试编程并使用多个线程,但没有失败.我想无法在我的机器上模拟比赛.
这并不意味着什么!问题是如果JLS 允许a
其他线程更新的执行,则代码不是线程安全的.事实上,您不能在特定计算机和特定Java实现上使用特定测试用例导致竞争条件发生,并不排除在其他情况下发生这种情况.
这是否意味着!= a可以返回
true
.
是的,从理论上讲,在某些情况下.
或者,即使同时改变a != a
也可以返回.false
a
关于"怪异行为":
当我的程序在一些迭代之间开始时,我得到输出标志值,这意味着引用!=检查在同一个引用上失败.但是在一些迭代之后,输出变为常量值false,然后长时间执行程序不会产生单个真输出.
这种"怪异"行为与以下执行方案一致:
程序已加载,JVM开始解释字节码.由于(正如我们从javap输出中看到的)字节码执行两次加载,您(显然)偶尔会看到竞争条件的结果.
一段时间后,代码由JIT编译器编译.JIT优化器注意到有两个相同内存槽(a
)的负载靠近在一起,并优化第二个负载.(事实上,它有可能完全优化测试...)
现在竞争条件不再显现,因为不再有两个负载.
请注意,这是所有有什么JLS允许Java的实现做是一致的.
@kriss评论如此:
看起来这可能是C或C++程序员所谓的"未定义行为"(依赖于实现).好像在像这样的角落里的Java中可能会有一些UB.
Java内存模型(在JLS 17.4中指定)指定了一组前提条件,在这些前提条件下,一个线程可以保证看到另一个线程写入的内存值.如果一个线程试图读取另一个线程写入的变量,并且不满足这些前提条件,那么可能存在许多可能的执行......其中一些可能是不正确的(从应用程序的要求的角度来看).换句话说,定义了一组可能的行为(即"格式良好的执行"),但我们不能说这些行为中的哪一个会发生.
如果代码的最终结果相同,则允许编译器组合并重新排序加载并保存(并执行其他操作):
但是,如果代码没有正确同步(因此"之前发生"关系不足以约束一组格式正确的执行),则允许编译器以可能产生"不正确"结果的方式重新排序加载和存储.(但这只是说程序不正确.)
Arn*_*lle 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);
}
}
Run Code Online (Sandbox Code Playgroud)
我有2次失败的10 000次调用.所以不,它不是线程安全的
ste*_*hke 15
不它不是.对于比较,Java VM必须将两个值放在堆栈上进行比较并运行compare指令(哪一个取决于"a"的类型).
Java VM可能:
false
在第一种情况下,另一个线程可以修改两次读取之间"a"的值.
选择哪种策略取决于Java编译器和Java Runtime(尤其是JIT编译器).它甚至可能在程序运行期间发生变化.
如果要确定如何访问变量,则必须进行变换volatile
(所谓的"半内存屏障")或添加完整的内存屏障(synchronized
).您还可以使用一些高级API(例如AtomicInteger
Juned Ahasan提到的).
有关线程安全性的详细信息,请阅读JSR 133(Java内存模型).
Stephen C.已经很好地解释了这一点.有趣的是,您可以尝试使用以下JVM参数运行相同的代码:
-XX:InlineSmallCode=0
Run Code Online (Sandbox Code Playgroud)
这应该阻止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
Run Code Online (Sandbox Code Playgroud)
不,a != a
不是线程安全的.该表达式由三部分组成:加载a
,a
再次加载和执行!=
.另一个线程可以获得a
父级的内部锁定并更改a
2个加载操作之间的值.
另一个因素是是否a
是本地的.如果a
是本地的,则没有其他线程可以访问它,因此应该是线程安全的.
void method () {
int a = 0;
System.out.println(a != a);
}
Run Code Online (Sandbox Code Playgroud)
也应该总是打印false
.
声明a
如volatile
不解决,如果问题a
是static
或实例.问题不在于线程具有不同的值a
,而是一个线程a
使用不同的值加载两次.它实际上可能使案例更少线程安全..如果a
不是volatile
那么a
可能会缓存,另一个线程的更改不会影响缓存的值.
归档时间: |
|
查看次数: |
10102 次 |
最近记录: |