为什么 java 中的 while(true) 在线程

问题描述

我正在使用其他 jframe 执行任务。现在,当我单击按钮添加新时,我有 2 个 jframe,在 jframe1。接下来,jframe2 将显示并且我想在单击时从这个 jframe 捕获事件,所以我使用线程和 while(true) 但它没有按我预期的那样工作,在我单击 jframe2 上的添加后,jframe1 上的线程没有捕获,它仍然循环无限的。我调试了,它工作正常,但是当我运行时,它不像我调试时那样工作。有什么问题吗?
这是我在 jframe1 中的代码

JButton btnNewButton = new JButton("add new");
    btnNewButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            Thread t = new Thread() {
                public void run() {
                    System.out.println("start");
                    Frame2 f2 = new Frame2();
                    do {
                        if (f2.getCheck() == 0) {
                            System.out.println("catched");
                            break;
                        }
                    } while (true);
                    System.out.println("end");
                };
            };
            t.start();
            
        }
    });

这里是我在 jframe2 中的代码

JButton btnNewButton = new JButton("add");
    btnNewButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            check = 0;
            frame.dispose();
        }
    });

解决方法

应该归咎于 Java 内存模型。

JVM 有一个邪恶的硬币。

它有时会抛硬币。具体来说,无论何时您访问任何字段,JVM 都会抛出邪恶的硬币。

Heads,JVM 会给你包含在这个字段中的值,这个值是它之前缓存的。它在该线程中拥有的值,无论其他线程写入它的任何更新。

Tails,它会为您提供任何线程最后写入的值。

这是邪恶的,因为它不是一个公平的硬币:今天,在您的单元测试期间,当调试器运行时,在这个特殊的月亮阶段,它每次都有效。明天和下周一样。但是 2 周后,就像您向重要客户提供演示一样?确实失败了。

关键是:如果你让 JVM 抛硬币,你就输了

不要让 JVM 翻转它。

要禁用JVM的邪恶硬币,您必须建立所谓的happens-before/happens-after关系。

HB/HA 关系的工作原理如下:

  • 对于整个代码库中任意 2 个语句,这 2 个语句要么具有 HB/HA,要么没有。
  • 如果他们确实有 HB/HA,那么 JVM 会向您保证,happens-after 行无法观察任何状态,因为它是 beforehappens-before 行,时间除外。换句话说,代码的执行就好像与“发生在”行具有“发生在之前”关系的语句实际上发生在之前一样。它实际上并不需要,但您无法观察到它没有,所以这无关紧要。
  • 但是,如果他们没有 HB/HA,任何事情都可能发生。您可能观察到,也可能没有观察到,即使您有绝对证据表明 B 行在 A 行之后运行,那么 B 行可能会或可能不会观察到 A 发生任何变化,JVM 自行决定:发生邪恶的硬币翻转。

您可能会发现这一切都非常愚蠢,而且显然很奇怪;为什么JVM会有一个邪恶的硬币,它为什么要玩这些游戏?

答案是效率。内核间通信非常昂贵。 JVM 会经常但不总是尝试有效地运行代码,这通常涉及本地缓存副本。 JVM 不会每时每刻都检查它是否无事可做,并以某种明智的方式积极地将所有这些缓存同步到一起,这将效率太低。 CPU 有其他事情要做,即使它们没有,嘿,笔记本电脑也是一回事,电费也是如此,数据中心的配置 CPU 周期计数也是如此,因此花费不必要的 CPU 周期是一种浪费不应该发生。

所以 JVM 没有。如果它愿意,它会很高兴地让事情不同步,或者不同步,在几天内自行决定。

就是你所观察到的。调试器启动后,各种效率都不会运行,因为它们会混淆调试器,因此您从线程 A 观察到 check = 0 线程 B 将其设置为该值后几秒。但是,如果没有打开调试器,您可能会打印一些信息,告诉您 B 已将 check 设置为 0,但随后 A 看到该检查连续几天都不是 0。

换句话说,在线程 B 中,check 似乎是 0。您可以打印时间并确认这发生在时间点 X。然而,即使它很清楚 strong> 很久很久以后,A 仍然 观察到 check 仍然是 3 或在 A 将其设置为 0 之前的任何内容。解释是 B 和 A 各自拥有自己的 check 副本。 JVM 不会只是因为你想要同步它们。并且没有简单的 syncItUp() 方法。而且,是的,也不能保证这些本地副本在那里。 JVM 可以自由地同步或不同步它们。打开调试器后,它会同步它们。没有它,它没有。很奇怪,但是 JVM 的规范赋予了 JVM 这样做的能力。邪恶的硬币。唯一的保证方法就是建立HB/HA。

要建立HB/HA,嗯,你可以在网上搜索。但是,简单的方法是:

  • synchronized 关键字:如果线程 A 退出在对象 ref 上同步到对象 X 的块,则保证“发生在”稍后进入此类同步块的任何其他线程之前。
  • 线程开始。如果线程 A 启动线程 B,则 b.start(); 之前的行相对于线程 B 中的第一行是 HB。
  • volatile。对 volatile 字段的写入建立 HB/HA 与读取,但您无法保证它是否实际运行更早(这些不是锁)。不过,在您的情况下,它在这里“有效”。
  • 任何在内部使用这些东西的 Java 库工具。例如,java.util.concurrent 包中的几乎所有内容。

所以,简而言之,使 volatile。更一般地说,尝试从不同的线程读取和写入同一字段是 Here Be Dragons,它们会咬你的头!领土。正确编写代码是很困难的,如果你搞砸了,单元测试将无法捕捉到它。您将拥有一个非常适合您的 flakey 应用程序,但在其他机器上或在奇怪的情况下会失败。

如果可以,请使用适当的工具(j.u.c 类是一个好的开始),并尽量不要从多个线程写入字段。例如,通过具有适当事务管理的数据库进行所有线程间通信。