【问题标题】:Java Swing: How do I wake up the main thread from the event-dispatch thread?Java Swing:如何从事件调度线程中唤醒主线程?
【发布时间】:2012-04-21 22:05:05
【问题描述】:

我想让“主线程”(运行main()的线程启动)从按钮的ActionListener的actionPerformed()方法做一些工作,但我不知道如何实现。

更多上下文:

我目前正在使用 Swing(俄罗斯方块的一种风格)编写 2D 游戏。
当应用程序启动时,会打开一个显示游戏主菜单的窗口。 向用户展示了几种可能性,其中一种是通过按下“开始”按钮来开始游戏,这会导致显示游戏面板并触发游戏的主循环。

为了能够在两个面板(主菜单和游戏)之间切换,我使用了 CardLayout 管理器,然后我可以通过调用 show() 来显示一个面板。
我的想法是我希望我的开始按钮有一个如下所示的侦听器:

public class StartListener implements ActionListener {
    StartListener() {}
    public void actionPerformed(ActionEvent e) {
        displayGamePanel();
        startGame();
    }
}

但这不起作用,因为actionPerformed()是从事件调度线程调用的,所以对startGame()的调用(触发主循环:游戏逻辑更新+repaint()在每一帧调用)阻塞了整个线程。

我现在处理这个问题的方式是 actionPerformed() 只是改变一个布尔标志值:public void actionPerformed(ActionEvent e) { startPushed = true; }
然后最终由主线程检查:

while (true) {
    while (!g.startPushed) {
        try { 
            Thread.sleep(100); 
        } catch (Exception e) {}
    }
    g.startPushed = false;
    g.startGame();
}

但我发现这个解决方案非常不雅。

我已经阅读了Concurrency in Swing 的课程,但我仍然感到困惑(我应该实现一个工作线程——这不是有点矫枉过正吗?)。我还没有完成任何实际的多线程工作,所以我有点迷茫。

有没有办法告诉主线程(它将无限期地休眠,等待用户操作)“好的,现在醒来并执行此操作(显示游戏面板并开始游戏)”?。

感谢您的帮助。

编辑: 为了清楚起见,这就是我的游戏循环的样子:

long lastLoopTime = System.currentTimeMillis();
long dTime;
int delay = 10;
while (running) {
    // compute the time that has gone since the last frame
    dTime = System.currentTimeMillis() - lastLoopTime;
lastLoopTime = System.currentTimeMillis();

    // UPDATE STATE
    updateState(dTime);
    //...

    // UPDATE GRAPHICS
    // thread-safe: repaint() will run on the EDT
    frame.repaint()

    // Pause for a bit
    try { 
    Thread.sleep(delay); 
    } catch (Exception e) {}
}

【问题讨论】:

    标签: java multithreading swing events event-dispatch-thread


    【解决方案1】:

    这没有意义:

    但这不起作用,因为 actionPerformed() 是从事件调度线程调用的,所以对 startGame() 的调用(触发主循环:每帧的游戏逻辑更新 + repaint() 调用)会阻塞整个线程。

    因为您的游戏循环不应阻塞 EDT。您是否在游戏循环中使用 Swing Timer 或后台线程?如果没有,请这样做。

    关于:

    while (true) {
        while (!g.startPushed) {
            try { 
                Thread.sleep(100); 
            } catch (Exception e) {}
        }
        g.startPushed = false;
        g.startGame();
    }
    

    也不要这样做,而是使用侦听器来处理这类事情。

    例如,

    import java.awt.CardLayout;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    
    import javax.swing.*;
    
    public class GameState extends JPanel {
       private CardLayout cardlayout = new CardLayout();
       private GamePanel gamePanel = new GamePanel();
       private StartPanel startpanel = new StartPanel(this, gamePanel);
    
       public GameState() {
          setLayout(cardlayout);
          add(startpanel, StartPanel.DISPLAY_STRING);
          add(gamePanel, GamePanel.DISPLAY_STRING);
       }
    
       public void showComponent(String displayString) {
          cardlayout.show(this, displayString);
       }
    
       private static void createAndShowGui() {
          GameState mainPanel = new GameState();
    
          JFrame frame = new JFrame("GameState");
          frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          frame.getContentPane().add(mainPanel);
          frame.pack();
          frame.setLocationByPlatform(true);
          frame.setVisible(true);
       }
    
       public static void main(String[] args) {
          SwingUtilities.invokeLater(new Runnable() {
             public void run() {
                createAndShowGui();
             }
          });
       }
    }
    
    class StartPanel extends JPanel {
       public static final String DISPLAY_STRING = "Start Panel";
    
       public StartPanel(final GameState gameState, final GamePanel gamePanel) {
          add(new JButton(new AbstractAction("Start") {
    
             @Override
             public void actionPerformed(ActionEvent e) {
                gameState.showComponent(GamePanel.DISPLAY_STRING);
                gamePanel.startAnimation();
             }
          }));
       }
    }
    
    class GamePanel extends JPanel {
       public static final String DISPLAY_STRING = "Game Panel";
       private static final int PREF_W = 500;
       private static final int PREF_H = 400;
       private static final int RECT_WIDTH = 10;
       private int x;
       private int y;
    
       public void startAnimation() {
          x = 0;
          y = 0;
          int timerDelay = 10;
          new Timer(timerDelay , new ActionListener() {
    
             @Override
             public void actionPerformed(ActionEvent e) {
                x++;
                y++;
                repaint();
             }
          }).start();
       }
    
       @Override
       protected void paintComponent(Graphics g) {
          super.paintComponent(g);
          g.fillRect(x, y, RECT_WIDTH, RECT_WIDTH);
       }
    
       @Override
       public Dimension getPreferredSize() {
          return new Dimension(PREF_W, PREF_H);
       }
    }
    

    【讨论】:

    • startGame()调用触发器游戏循环,所以游戏循环在调用startGame()的线程上执行。因此,如果我从actionPerformed() 调用startGame(),它确实会阻塞EDT,但是当我从main()(这是我放置while(true)... 的位置)调用它时,它不会阻塞EDT 并且效果很好。另外,EDT 中没有使用摆动计时器(这不是我想要做的)?我想使用监听器之类的东西,但我知道的唯一监听器在 EDT 上运行,我想要在我的主线程上运行的监听器。
    • @Rez:同样,游戏循环不应阻塞 EDT。时期。 Swing Timers 在 EDT 上运行,但定时器延迟部分,隐藏的Thread.sleep(...) 没有,这是关键。请参阅上面的编辑以回答更多信息。
    • 感谢您的回答(我说的是游戏循环不会阻止 EDT)。我已经阅读并运行了您的代码,看来您在 EDT 上所做的所有工作(小心使用计时器以免阻塞整个线程)? (我刚刚检查了SwingUtilities.isEventDispatchThread()startAnimation() 和相关actionPerformed 中的工作是在 EDT 上完成的)。我认为我们不应该在 EDT 中完成所有繁重的工作(游戏逻辑、状态更新等等),而是在一个单独的线程中,不是吗?对不起,我有点困惑。再次感谢。
    • @Rez:繁重的工作,比如读入文件、写入文件、从套接字获取字节等等......应该在后台线程上完成,但我不会考虑大多数游戏逻辑和状态更新很繁重。
    • 在 EDT 上更新游戏状态也简化了线程问题。否则你需要在 updateState 方法运行时小心绘画。
    【解决方案2】:

    您应该使用SwingWorker,这将在后台线程中执行doInBackground() 中的代码,并在doInBackground() 停止后在EDT 中执行done() 中的代码

    【讨论】:

      【解决方案3】:

      最简单的方法:使用CountDownLatch。您将其设置为 1,以任何适当的方式使其在 Swing 代码中可用,并在主线程中 await 它。

      【讨论】:

      • 谢谢,我现在正在考虑这个可能性,这似乎正是我所期望的 :)
      【解决方案4】:

      您可以考虑使用 SwingUtilities.invokeAndWait() 在游戏面板中显示模式对话框,以便在关闭对话框时控件返回主线程。

      【讨论】:

      • 谢谢,这应该可以,但是如果可能的话,我宁愿只使用一个菜单面板(我也想只保留一个窗口,我不确定对话框是否很好嵌入小程序)?
      • 嗯,我的回答是基于您希望关闭第一个面板(即让用户进行选择)然后开始另一个显示的想法。可能正确的方法是停止在主线程中做任何事情,并使用其他人已经建议的摆动计时器异步地做所有事情。通常在 Swing 应用程序中,主线程的工作有限,只是将框架或对话框设置为可见。
      【解决方案5】:

      您可以使除 EDT 之外的所有代码在单线程执行服务上运行,然后在需要执行某些代码时发布可运行文件。

      【讨论】:

        猜你喜欢
        • 2014-07-15
        • 2018-12-04
        • 2017-02-13
        • 1970-01-01
        • 2020-05-21
        • 2014-07-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多