检查线程是否安全?

我知道像 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
10846 次浏览

检查 a != a线程安全吗?

如果 a可能被另一个线程更新(没有正确的同步!) ,那么 No。

我尝试编写这个程序并使用多个线程,但没有失败。我猜不能在我的机器上模拟比赛。

这说明不了什么!问题是,如果由另一个线程更新 a的执行是由 JLS 更新的 允许,那么代码就不是线程安全的。您不能使竞态条件在特定机器和特定 Java 实现上的特定测试用例中发生,这一事实并不妨碍在其他情况下发生竞态条件。

这是否意味着! = a 可以返回 true

是的,理论上,在某些情况下。

另外,即使 a同时发生变化,a != a也可以返回 false


关于“怪异行为”:

当我的程序在一些迭代之间启动时,我得到输出标志值,这意味着引用!= 在同一引用上检查失败。但是在一些迭代之后,输出变为常量值 false,然后长时间执行程序不会产生一个真输出。

这种“奇怪”的行为符合以下执行场景:

  1. 程序加载后,JVM 启动字节码 翻译。因为(正如我们从 javap 输出中看到的)字节码执行两次加载,所以(显然)偶尔会看到竞态条件的结果。

  2. 一段时间后,代码由 JIT 编译器编译。JIT 优化器注意到有两个相同内存槽(a)的负载靠得很近,因此优化了第二个内存槽。(事实上,它有可能完全优化了测试... ...)

  3. 现在竞态条件不再显示,因为不再有两个负载。

注意,这与 JLS 允许 Java 实现做的事情是一致的。


@ kriss 评论道:

这看起来像是 C 或 C + + 程序员所说的“未定义行为”(依赖于实现)。看起来像这样的角落案件可能有一些在爪哇的 UB。

Java 内存模型(在 JLS 17.4中指定)指定了一组前提条件,在这些前提条件下,一个线程可以保证看到另一个线程写入的内存值。如果一个线程试图读取另一个线程编写的变量,而这些先决条件没有得到满足,那么可能会有许多可能的执行... 其中一些可能是不正确的(从应用程序需求的角度来看)。换句话说,定义了可能行为的 准备好了(即“良好形式的执行”集合) ,但是我们不能说哪些行为会发生。

如果代码的最终效果相同,编译器可以组合和重新排序加载和保存(以及做其他事情) :

  • 当由单个线程执行时,
  • 当由正确同步的不同线程执行时(按照内存模型)。

但是如果代码没有正确地同步(因此“之前发生”的关系不能充分地约束格式良好的执行集) ,编译器就可以重新排序加载和存储,从而得到“不正确”的结果。(但这实际上只是说程序是不正确的。)

在没有同步的情况下,这段代码

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加载到本地 vars 两次,这是一个非原子操作,如果 a在两次操作之间被另一个线程比较改变,可能会产生 false

此外,内存可见性问题在这里是相关的,不能保证另一个线程对 a所做的更改对当前线程是可见的。

不,不是的。为了进行比较,JavaVM 必须将要进行比较的两个值放在堆栈上,并运行比较指令(该指令取决于“ a”的类型)。

Java 虚拟机可以:

  1. 读“ a”两次,把每个都放到堆栈上,然后比较结果
  2. 只读取“ a”一次,将其放在堆栈上,复制它(“ dup”指令)并运行比较
  3. 完全消除表达式,并用 false替换它

在第一种情况下,另一个线程可以在两次读之间修改“ a”的值。

选择哪种策略取决于 Java 编译器和 Java 运行时(尤其是 JIT 编译器)。它甚至可能在程序运行期间发生更改。

如果要确定如何访问变量,必须将其设置为 volatile(所谓的“半内存屏障”)或添加完整内存屏障(synchronized)。您也可以使用一些更高级别的 API (例如 Juned Ahasan 提到的 AtomicInteger)。

有关线程安全的详细信息,请参阅 JSR 133(Java 内存模型)。

通过测试证明:

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个失败在10000调用。所以 没有,它是 没有线程安全

关于这种奇怪的行为:

由于变量 a没有标记为 volatile,因此在某些时候,线程可能会缓存 a的值。a != a的两个 a都是缓存版本,因此总是相同的(这意味着 flag现在总是 false)。

不,a != a不是线程安全的。该表达式由三部分组成: 加载 a、再次加载 a和执行 !=。另一个线程可以在 a的父线程上获得内部锁,并在两次加载操作之间更改 a的值。

另一个因素是 a是否是本地的。如果 a是本地的,那么就没有其他线程可以访问它,因此应该是线程安全的。

void method () {
int a = 0;
System.out.println(a != a);
}

也应该总是打印 false

a声明为 volatile并不能解决 astatic或实例的问题。问题不在于线程具有不同的 a值,而在于一个线程使用不同的值两次加载 a。它实际上可能会降低案例的线程安全性。.如果 a不是 volatile,那么 a可能会被缓存,另一个线程的更改不会影响缓存的值。

Stephen C 已经很好地解释了这一切,为了好玩,您可以尝试使用以下 JVM 参数运行相同的代码:

-XX:InlineSmallCode=0

这将阻止 JIT 所做的优化(它在热点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

即使是简单的读也不是原子的。如果 along并且没有标记为 volatile,那么在32位 JVM long b = a上就不是线程安全的。