!= 检查线程安全吗?

我知道诸如之类的复合操作不是线程安全的,因为它们涉及多个操作。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();
    }

}

这是我用来测试线程安全性的程序。

奇怪的行为

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

正如输出所建议的那样,经过一些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

答案 1

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

Object a;

public boolean test() {
    return a != a;
}

可能产生 .这是 的字节码truetest()

    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    IF_ACMPEQ L1
...

正如我们所看到的,它将字段加载到本地vars两次,这是一个非原子操作,如果在两者之间被另一个线程进行比较可能会产生。aafalse

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


答案 2

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

如果可以由另一个线程更新(没有适当的同步!),则否。a

我试图对此进行编程并使用多个线程,但没有失败。我想无法在我的机器上模拟比赛。

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

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

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

或者,即使同时发生变化,也可以返回。a != afalsea


关于“怪异行为”:

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

这种“奇怪”的行为与以下执行方案一致:

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

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

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

请注意,这与JLS允许Java实现的功能完全一致。


@kriss这样评论:

这看起来像是C或C++程序员所说的“未定义行为”(依赖于实现)。似乎在像这样的角落情况下,Java中可能会有一些UB。

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

允许编译器组合和重新排序加载和保存(并执行其他操作),前提是代码的最终效果相同:

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

但是,如果代码没有正确同步(因此“发生在之前”的关系不能充分约束格式良好的执行集),则允许编译器以产生“不正确”结果的方式对加载和存储进行重新排序。(但那实际上只是说程序不正确。