【问题标题】:Putting a breakpoint in a non reachable thread forces it to run在不可访问的线程中放置断点会强制它运行
【发布时间】:2018-12-28 21:35:16
【问题描述】:

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

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!"); 行设置断点时,它会到达那里并打印该行!设置断点到底发生了什么?它是否强制程序直接从内存中读取变量的值?还是发生了其他事情?

【问题讨论】:

  • Breakpoint只是用来调试java应用或者类,和内存中的变量没有任何关系
  • @Deadpool 据我所知,断点通过添加一个名为INT 3 的特殊陷阱来更改代码指令。那么到底发生了什么?

标签: java multithreading debugging breakpoints


【解决方案1】:

正如我所料,由于布尔测试不是易失性的,Thread1 使用本地缓存值 test,当 Thread2 将其更改为 true 时,Thread1 不会做任何事情。

你的预期不正确。

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

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


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

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


据我所知,断点通过添加一个名为 INT 3 的特殊陷阱来更改代码指令。那么到底发生了什么?

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


当我在 if 语句之前的 Thread1 中添加 sleep(1) 时,它也会打印该行。添加睡眠是否会发生同样的事情?

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

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

但是,我要强调的是,这些行为并未具体说明。


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

【讨论】:

  • 谢谢大佬,还有一个不明确的地方。当我在 if 语句之前的 Thread1 中放置 sleep(1) 时,它也会打印该行。添加睡眠是否会发生同样的事情?我猜想当 Thread1 重新开始时中断睡眠时,它将使用新的测试值。我说的对吗?
【解决方案2】:

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

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


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

“使用测试的本地缓存值”是优化(可能在 JIT 时间) - 因此当您开始调试代码(或启用某种带断点的单步执行)时,它将关闭优化并每次从内存中读取因此基本上使变量接近volatile(仍然没有必要一直以这种方式表现,但接近)。

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

【讨论】:

  • 感谢您的回答,但它不会在调试模式下打印,它只会在我放置断点时打印,当我删除断点时它不会再打印了!
  • @Spara 我希望 Java 调试器比 .Net 更智能一次(我的答案确实基于此),并且当您想在其中添加断点时重新 JIT 单一方法。尝试禁用优化 (stackoverflow.com/questions/5242405/…) 并查看是否得到与断点相同的行为。
  • 禁用优化将没有线程缓存,因此它将始终打印带或不带断点的行
  • @Spara 是的,这就是我相信会发生的事情 - 当您想要逐步执行一个方法时,它必须被取消优化,并且其行为方式与第一次没有启用优化一样地方。我发现的关于 JVM 行为的唯一信息是关于去优化代码抛出异常 - 所以你需要 JVM+JVMTI+JDI 专家来真正回答这个问题。我建议尽可能增加赏金。
  • @Spara 旁注:我会非常担心编写显式依赖运行时优化行为的代码(我假设您只是将此作为奇怪行为的示例,而不是您实际使用的东西正在生产中)。
【解决方案3】:

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

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

【讨论】:

  • 所以你的意思是减慢应用程序的速度,优化不会做任何事情?
  • 我在 Thread1 的 if 语句顶部添加了 for (int i = 0 ; i < Integer.MAX_VALUE; i++); 并打印了该行。因此,如果阈值通过,JVM 似乎不会优化线程。
  • @Spara 这个循环将触发方法根据它所拥有的运行时信息进行编译(即其余代码没有,因为它没有运行它)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-18
  • 2014-06-09
相关资源
最近更新 更多