【问题标题】:Why is this never throwing an AssertionError even after running it for so long?为什么即使运行了这么长时间也不会抛出 AssertionError ?
【发布时间】:2018-06-23 03:29:27
【问题描述】:

这是原始代码

//@author Brian Goetz and Tim Peierls
@ThreadSafe
public class SafePoint {
    @GuardedBy("this") private int x, y;

    private SafePoint(int[] a) {
        this(a[0], a[1]);
    }

    public SafePoint(SafePoint p) {
        this(p.get());
    }

    public SafePoint(int x, int y) {
        this.set(x, y);
    }

    public synchronized int[] get() {
        return new int[]{x, y};
    }

    public synchronized void set(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

这里很好,私有 int x,y 不是最终的,因为构造函数中的 set 方法在调用 get 时会导致发生在关系之前,因为它们使用相同的锁。

现在这里是修改后的版本和一个 main 方法,我希望在运行一段时间后抛出一个 AssertionError,因为我删除了 set 方法中的 synchronized 关键字。我将构造函数设为私有,以便成为唯一调用它的人,以防有人指出它因此不是线程安全的,这不是我问题的重点。

无论如何,我现在已经等了很久,没有抛出任何 AssertionErrors。现在我厌倦了这个修改后的类在某种程度上是线程安全的,即使根据我所学到的,这并不是因为 x 和 y 不是最终的。有人能告诉我为什么 AssertionError 仍然没有被抛出吗?

public class SafePointProblem {
    static SafePoint sp = new SafePoint(1, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                final int finalI = new Random().nextInt(50);
                new Thread(() -> {
                    sp = new SafePoint(finalI, finalI);
                }).start();
            }
        }).start();
        while (true) {
            new Thread(() -> {
                sp.assertSanity();
                int[] xy = sp.get();
                if (xy[0] != xy[1]) {
                    throw new AssertionError("This statement is false 1.");
                }
            }).start();
        }
    }
}

class SafePoint {
    private int x, y;

    public SafePoint(int x, int y) {
        this.set(x, y);
    }

    public synchronized int[] get() {
        return new int[]{x, y};
    }

    // I removed the synchronized from here
    private void set(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void assertSanity() {
        if (x != y) {
            throw new AssertionError("This statement is false2.");
        }
    }
}

【问题讨论】:

    标签: java multithreading concurrency visibility jit


    【解决方案1】:

    我不确定这个问题是否可以仅使用JMM 来回答,因此您很可能会遇到某种未定义的行为。

    为了更深入地研究这个问题,我们可以尝试反编译它。我运行了这个用 HotSpot C2 编译器编译的代码。这是我能找到的片段(整个编译代码太长):

      0x00007f6b38516fbd: lock addl $0x0,(%rsp)     ;*synchronization entry
                                                    ; - java.util.Random::<init>@-1 (line 105)
                                                    ; - com.test.SafePointProblem$lambda::run@4 (line 19)
    
      0x00007f6b38516fc2: mov     0x10(%r10),%rax   ;*invokevirtual compareAndSwapLong
                                                    ; - java.util.concurrent.atomic.AtomicLong::compareAndSet@9 (line 147)
                                                    ; - java.util.Random::next@32 (line 204)
                                                    ; - java.util.Random::nextInt@17 (line 390)
                                                    ; - com.test.SafePointProblem$lambda
    

    我不是 HotSpot JIT 编译器专家,但据我所知,编译后的代码包含您所有可运行文件中的同步。其中一些来自Random::next(它使用 CAS),它是原子的并重置 CPU 存储缓冲区。

    “为什么?”这个问题的详尽答案可能相当复杂,而且绝对依赖于平台。

    【讨论】:

    • 这是一个非常有趣的答案。我可能需要使用递增计数器而不是使用 random.nextint 来执行 forloop,然后看看会发生什么 Upvoted
    • @St.Antario 我真的不知道这段代码实际上证明了什么。 lock addlStoreLoad 障碍。什么意思?
    • @St.Antario 也有一小部分代码不能证明规范(或相反),因此即使某个编译器做某事,其他编译器也可能不会。唯一要遵守的是规范。
    • @Eugene 只是更仔细地看了一下代码。不得不同意,简单的负载围栏在这里并不能证明一致性。此外,我看不到如何在此处获取AssertionError
    • @Eugene 因为读/写引用是原子的和新的finalI=new Random().nextInt(50) hb new SafePoint(finalI, finalI) 当引用被发布时(甚至不安全)如果没有prefetchnta 指令用过……我看不出有什么办法……
    【解决方案2】:

    你已经运行了这个很长时间这个事实并不意味着什么,它只是意味着此刻你没有重现这个;可能使用不同的jreCPU 这可能会中断。尤其糟糕,因为墨菲定律将保证这将在生产中的某个地方发生,并且您将面临调试的噩梦。

    一个小例子并不能证明代码是好的/正确的,尤其是并发代码——这非常困难(我什至不敢说我完全理解它)。而且您确实了解这可能很糟糕,因为没有发生之前。

    同时使这些变量final 意味着您不能通过setters 设置它们,而只能在构造函数中设置。所以这意味着你不能有一个设置器,因此一旦设置了xy 字段,就没有人可以更改它们,因此get 根本不应该被同步(我在这里说的是你的SafePoint

    【讨论】:

    • 实际上我试图调查是否有可能抛出AssertionError。也许 HotSpot 服务器编译器进行了优化,但仍然不允许抛出 Error。我发现的只是同步点来自Random 并启动线程......不太有用......
    • 也赞成,是的,我知道最终字段及其约束。很高兴听到在不同的 jre 或 cpu 上这可能会如何中断,并感谢您确认之前发生的担忧
    • @katiex7 np,但我无法真正了解接受的答案(我不在乎你接受哪个)解释事情。 lock addlsynchronization entry 触发了吗?
    • 啊,我认为 random.nextint 是我能听到的保证之前发生的事情
    • @katiex7 不要误会我的意思,但是C1 编译器将生成的任何代码在这种情况下都无关紧要,唯一可以依赖的是规范 - 和正如您已经得出相同的结论,您的代码在 JLS 规则下是不正确的,IMO 是这里唯一重要的东西
    猜你喜欢
    • 2021-06-19
    • 1970-01-01
    • 2019-11-02
    • 2021-05-28
    • 1970-01-01
    • 2016-09-29
    • 2023-04-04
    • 2012-07-03
    • 1970-01-01
    相关资源
    最近更新 更多