!=检查线程是否安全?

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当前线程对另一个线程所做的更改是可见的.

  • 虽然有力的证据,字节码实际上并不是一个证明.它必须在JLS的某个地方...... (22认同)
  • 实际上,假设生成的字节码符合JLS,那就是证明! (20认同)
  • @Marko我同意你的想法,但不一定是你的结论.对我来说,上面的字节码是实现`!=`的明显/规范方式,它涉及分别加载LHS和RHS.因此,如果JLS*在LHS和RHS语法相同时没有提及有关优化的任何具体内容,那么将适用一般规则,这意味着加载"a"两次. (10认同)
  • @Adrian:首先:即使这个假设是无效的,它可以评估为"true"的*single*编译器的存在足以证明它有时可以评估为"true"(即使规范禁止它 - 它没有).其次:Java是明确的,大多数编译器都与它紧密相关.在这方面使用它们作为参考是有意义的.第三:你使用术语"JRE",但我认为这并不意味着你的意思... (6认同)
  • @AlexanderTorstling - *"我不确定这是否足以排除单读优化."*这还不够.实际上,在没有同步(以及强加的"之前发生的"关系)的情况下,优化是有效的, (2认同)

Ste*_*n C 47

检查是否是a != a线程安全的?

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

我尝试编程并使用多个线程,但没有失败.我想无法在我的机器上模拟比赛.

这并不意味着什么!问题是如果JLS 允许a其他线程更新的执行,则代码不是线程安全的.事实上,您不能在特定计算机和特定Java实现上使用特定测试用例导致竞争条件发生,并不排除在其他情况下发生这种情况.

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

是的,从理论上讲,在某些情况下.

或者,即使同时改变a != a也可以返回.falsea


关于"怪异行为":

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

这种"怪异"行为与以下执行方案一致:

  1. 程序已加载,JVM开始解释字节码.由于(正如我们从javap输出中看到的)字节码执行两次加载,您(显然)偶尔会看到竞争条件的结果.

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

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

请注意,这是所有有什么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次调用.所以,它不是线程安全的

  • 你甚至没有检查相等性......`Random.nextInt()`部分是多余的.您也可以使用`new Object()`进行测试. (6认同)

ste*_*hke 15

不它不是.对于比较,Java VM必须将两个值放在堆栈上进行比较并运行compare指令(哪一个取决于"a"的类型).

Java VM可能:

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

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

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

如果要确定如何访问变量,则必须进行变换volatile(所谓的"半内存屏障")或添加完整的内存屏障(synchronized).您还可以使用一些高级API(例如AtomicIntegerJuned Ahasan提到的).

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


ass*_*ias 6

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)


Dou*_*Mx2 5

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

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

void method () {
    int a = 0;
    System.out.println(a != a);
}
Run Code Online (Sandbox Code Playgroud)

也应该总是打印false.

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