在不可访问的线程中放置断点会强制它 运行

Putting a breakpoint in a non reachable thread forces it to run

这段代码有一个奇怪的问题:

class Test {
    private static boolean test = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (test) {
                    System.out.println("Print when breakpoint here!");
                    test = false;
                }
            }
        }, "Thread1").start();

        new Thread(() -> {
            while (true) {
                System.out.println("Print always");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test = true;
            }
        }, " Thread2").start();
    }
}

如我所料,由于 boolean test 不是 volatile,线程 1 使用 test 的本地缓存值,当线程 2 将其更改为 true 时,线程 1 不会执行任何事物。但是当我在 System.out.println("Prints when put a breakpoint here!"); 行放置一个断点时,它会到达那里并打印该行!设置断点到底发生了什么?它是否强制程序直接从内存中读取变量的值?或者发生了其他事情?

警告: 这个答案主要基于 .Net 调试器的工作方式,但我预计两次 运行 之间会有类似的行为。我希望 JVM 允许在 运行 时间对每个方法重新进行 JIT,因为它已经可以用 HotSpot JIT 替换方法。

有一些现有的文章和帖子介绍了为调试关闭了哪些优化,例如 AMD: perf when debugging enabled, Side Effects of running the JVM in debug mode, Will Java app slow down by presence of -Xdebug or only when stepping through code?。他们暗示至少当有异常代码在调试器下采用明显不同的代码路径时,可能如何实现断点。


许多调试器会在您调试代码时关闭优化(如果您允许重新编译代码则为编译时间,如果您调试现有代码则为 JIT 时间)。在 .Net 世界中,影响是全局的——当附加调试器时,它可以将所有未来的 JIT 编译切换到非优化路径,我希望 Java/JVM 支持更精细的控制,以允许仅取消优化可能需要停止的方法调试器。这样做是为了让您清楚地看到所有变量的所有值。否则一半的信息有时包括方法调用和 local/member 变量不可用。

"uses local cache value of test" 是优化(可能在 JIT 时间)——所以当您开始调试代码(或使用断点启用某种单步执行)时,它会关闭优化并每次都从内存中读取本质上是使变量接近 volatile(仍然没有必要一直以这种方式运行但接近)。

根据您使用的调试器,您可以禁用此类行为(但调试会更加困难)。

As I expected, since boolean test is not volatile Thread1 uses local cache value of test and when Thread2 changes it to true Thread1 won't do anything.

您的期望不正确。

根据 Java 语言规范,如果一个线程更新了一个非易失性共享变量,而另一个线程随后在没有适当同步的情况下读取它,那么第二个线程 可能 查看新值,或者它可能会看到更早的值。

所以你看到的是 JLS 允许的。


实际上,当调试代理附加到 JVM 时,它通常会导致 JIT 编译器以较低的优化级别重新编译部分或所有方法……甚至可能使用字节码解释器执行它们。对于在其中设置了断点的方法以及单步执行 1 的方法,很可能会发生这种情况。对于在调试时使用共享变量但未正确同步的代码,这可能会导致 不同的行为

这也是同步不充分导致调试困难的原因之一


As far as I know breakpoints change the instructions of code by adding a special trap called INT 3. So what's really going on?

这就是调试 C/C++ 时发生的情况。没有指定 JVM 如何处理这个问题,但是典型的 JVM 有其他选项来实现断点......因为字节码和 JIT 编译。


When I put a sleep(1) in the Thread1 before if statement it will also print the line. Is there a same thing happening by adding a sleep?

sleep会导致当前线程被挂起。 未指定在实施层面发生的事情。但是,本机线程机制 可能 会将挂起线程的任何未完成写入(即脏缓存条目)刷新到内存中......作为执行线程上下文过程的一部分切换。

同样,如果您使用打印语句,典型的 I/O 堆栈具有内部同步,可以触发缓存刷新等。这也可以改变您尝试调试的代码的行为。

但是,我要强调的是,这些行为并不是特定的。


1 - 允许 JIT 优化器重新排序分配,前提是这不会改变单线程行为。但是,如果您正在调试一个方法并观察变量的值,则重新排序的效果是可见的(对程序员而言)。去优化/解释避免了这种情况。幸运的是,现代 JVM / 调试代理可以根据需要执行此操作 "on the fly"。

当您 运行 编写代码时,它会经历几个优化阶段。最初它被解释并且您的布尔变量可能始终可见,但是在最高级别的优化中,该值可以内联(不仅仅是缓存)并且您永远看不到变化。发生这种情况的时间点取决于代码循环了多少次以及代码何时在后台编译和替换。

当您添加日志记录或断点或睡眠 (1) 时,您会减慢应用程序并需要更长的时间才能达到代码优化的阈值。这意味着您可以避免看到问题,尤其是在竞争条件下。