【问题标题】:Java unable to break out of while loopJava 无法跳出 while 循环
【发布时间】:2020-04-17 22:20:52
【问题描述】:

我正在尝试在 java 中构建一个计时器。

预期输出:

当你运行程序时,会出现一个窗口,里面有一个大的“0”。

一旦用户按下键盘上的任意键,数字就会每秒增加 1。

这就是我所拥有的。

    import javax.swing.*;
    import javax.swing.SwingConstants;
    import java.awt.BorderLayout;
    import java.awt.Font;
    import java.awt.event.KeyListener;
    import java.awt.event.KeyEvent;

    public class TimerTest implements KeyListener {
        static final int SCREEN_WIDTH = 960;
        static final int SCREEN_HEIGHT = 540;
        static boolean timerStarted;
        static int keyPressedNum; //amount of times the user pressed any key.

        static JLabel label;
        static int currentTime;

    public static void main(String[] args) {

        JFrame frame = new JFrame("TimerTest");
        JPanel panel = new JPanel();

        label = new JLabel();
        label.setFont(new Font("Arial", Font.PLAIN, 256));
        label.setText("0");
        label.setHorizontalAlignment(SwingConstants.CENTER);

        panel.setLayout(new BorderLayout());
        panel.add(label, BorderLayout.CENTER);

        frame.addKeyListener(new TimerTest());

        frame.getContentPane().add(panel);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
        frame.setResizable(false);
        frame.setVisible(true);

        while (true) {
            while (TimerTest.timerStarted == false) {
                if (TimerTest.timerStarted == true) {break;}
            }

            try {Thread.sleep(1000);}
            catch (InterruptedException ex) {ex.printStackTrace();}

            currentTime++;
            label.setText(String.valueOf(currentTime));
        }
    }

    public void keyPressed(KeyEvent e) {
        System.out.println("Keypress indicated.");
        TimerTest.timerStarted = true;
    }

    public void keyReleased(KeyEvent e) {}
    public void keyTyped(KeyEvent e) {}
}

当我按下任意键时,程序会将timerStarted 设置为真。

因为timerStarted 是真的,所以我期待这部分:

    while (TimerTest.timerStarted == false) {
            if (TimerTest.timerStarted == true) {break;}
    }

跳出循环。

当我运行程序并按任意键时,命令行只会打印:Keypress indicated.,并且不会跳出循环。

现在是奇怪的部分:

我尝试在 while 块中添加一些代码,像这样。

  while (TimerTest.timerStarted == false) {
        System.out.println("currently stuck here...");
        //(I simply added a println code)
        if (TimerTest.timerStarted == true) {break;}
  }

令人惊讶的是,当我这样做时,程序就像我想要的那样运行。它跳出那个循环,计时器开始运行。

这让我很困惑。添加System.out.println(); 可以解决问题...?

为什么没有System.out.println() 会让我无法跳出循环?

任何解释将不胜感激 =)

【问题讨论】:

  • TimerTest.timerStarted == true 从逻辑上讲永远不会落入TimerTest.timerStarted == false 之下(除非在两个if 处决之间产生可预测的变化,无法想象的精确)
  • 是的,我知道这与没有TimerTest.timerStarted == true 几乎是一回事。但是,这两种情况都不起作用。
  • 您想要跳出循环的实际/真实条件是什么?我在循环中看不到任何改变TimerTest.timerStarted 的值。外面改了吗?
  • 我想在timerStarted = true 时跳出循环。我使用 while 循环“等待”直到 timerStarted 等于 true
  • IIRC,break 将跳出最近的循环,而不是所有循环。你的while 中有一个while,所以当你突破内部while 时,你应该仍然在外部while。

标签: java


【解决方案1】:

问题是您的代码不是线程安全的。具体来说,您没有做确保timerStarted 的读取看到另一个线程最近写入的操作。

有多种可能的解决方案,包括:

  • 将timerStarted 声明为volatile,或
  • 在synchronized 块中读取和写入timerStarted,或
  • 使用AtomicBoolean。

这些确保在一个线程写入标志和随后由第二个线程读取之间存在 happens-before 关系。这反过来又保证了读取会看到写入的值。

“为什么没有 System.out.println() 让我无法跳出循环?”

因为没有happens-before关系;见上文。

一个更有趣的问题是:

“为什么添加 println 会起作用?”

System.out.println() 在后台进行了一些同步。 (流对象是线程安全的,并且会进行同步以确保其数据结构不会混乱。)这显然足以导致对timerStarted 的写入刷新到主内存。

但是,这不能保证。 javadocs 没有为println 调用指定同步行为。此外,尚不清楚这是否足以在timerStarted 的写入和读取之间给出一个(偶然的!)发生在之前。

参考资料:

【讨论】:

    【解决方案2】:

    您的问题是因为您使用的是非线程安全 boolean 变量timerStarted

    此外,while(true) 循环是从主线程运行的,但您对 timerStarted 所做的修改是从另一个线程完成的,这会导致意外行为完全取决于运行程序的机器。

    这种情况在更改 timerStarted 的值和检查该更改之间产生竞争条件

    我是如何检测到这一点的:

    我只是在检查值更改和修改值时打印了线程 ID

    解决方案

    检查这些代码更改

    static AtomicBoolean timerStarted = new AtomicBoolean(false);
    

    和

       while (Scratch.timerStarted.get() == false) {
            System.out.println("Thread.ID: " + Thread.currentThread().getId() + " ----> Scratch.timerStarted: " + Scratch.timerStarted);
            if (Scratch.timerStarted.get() == true) {
                break;
            }
        }
    

    和

     public void keyPressed(KeyEvent e) {
            System.out.println("Keypress indicated.");
            Scratch.timerStarted.set(true);
            System.out.println("Thread.ID: " + Thread.currentThread().getId() + " ----> Scratch.timerStarted: " + Scratch.timerStarted);
        }
    

    这里我使用AtomicBoolean 而不是boolean,这是线程安全的。


    你需要阅读的内容:

    1. java 中的多线程(检查:https://www.javatpoint.com/multithreading-in-java)
    2. AtmoicBoolean 与布尔值(检查:When do I need to use AtomicBoolean in Java?)

    【讨论】:

      猜你喜欢
      • 2020-01-03
      • 2013-05-06
      • 1970-01-01
      • 2021-08-29
      • 2015-08-04
      • 1970-01-01
      • 2015-12-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多