【问题标题】:Java: Updating UI while waiting for another thread to finishJava:在等待另一个线程完成时更新 UI
【发布时间】:2013-01-11 10:24:16
【问题描述】:

我有一些 Java Swing 登录面板。当用户单击“登录”按钮时,会发生广泛的数据库通信。我有一个想法将数据库通信放入另一个线程(数据库通信用于填充一些哈希映射和单例)。将该通信放入单独的线程后,当用户输入它的名称和密码时,会发生 db 繁重的工作。当用户点击“登录”按钮时,代码将等待“繁重”线程加入,然后继续。

问题是,当我的代码正在等待数据库线程加入时,似乎无法进行任何 UI 更新。我认为我不能使用 SwingWorker 进行数据库通信,因为用户可以随时单击“登录”按钮,甚至在 SW 完成之前我也无法加入 SwingWorker(数据库通信必须在实际登录之前进行) .

有没有办法在等待另一个线程加入时启用 Swing ui 更新?我错过了什么吗?

【问题讨论】:

  • 1.您的意思是要在输入凭据并单击登录后更新 UI?比如更新进度条之类的?还是您只是希望用户能够输入凭据并单击登录,然后在数据库任务完成之前什么都不做?
  • 2.你目前使用什么java并发机制? (普通线程,线程池,执行器,...)?
  • 我使用的是普通线程。 @herman:我想开始加载“预登录”数据,然后检查 uname 并通过,但是如果用户快速输入凭据并单击登录,我想完成预加载,然后继续检查 uname 并通过
  • 如需尽快获得更好的帮助,请发帖SSCCE

标签: java swing event-dispatch-thread


【解决方案1】:

只需放置两个标志并在两个操作(数据库通信和用户点击)结束时,调用相同的方法并在那里验证两个标志的状态。如果您想阻止用户输入,可能会在模式对话框中显示进度条:

import java.awt.BorderLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UnsupportedLookAndFeelException;

public class TestThreadJoin {

    private boolean dbWorkDone = false;
    private boolean credentialsProvided = false;
    private JTextField loginTF;
    private JTextField passwordTF;

    protected void initUI() {
        final JFrame frame = new JFrame(TestThreadJoin.class.getSimpleName());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel panel = new JPanel(new GridBagLayout());
        JLabel login = new JLabel("Login: ");
        JLabel password = new JLabel("Password: ");
        loginTF = new JTextField(20);
        passwordTF = new JPasswordField(20);
        GridBagConstraints gbc = new GridBagConstraints();
        panel.add(login, gbc);
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        panel.add(loginTF, gbc);
        gbc.gridwidth = 1;
        panel.add(password, gbc);
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        panel.add(passwordTF, gbc);
        JButton loginButton = new JButton("Login");
        loginButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                credentialsProvided = true;
                proceed();
            }
        });
        frame.add(panel);
        frame.add(loginButton, BorderLayout.SOUTH);
        frame.pack();
        frame.setVisible(true);
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {

            @Override
            protected Void doInBackground() throws Exception {
                Thread.sleep(10000);
                return null;
            }

            @Override
            protected void done() {
                super.done();
                dbWorkDone = true;
                proceed();
            }
        };
        worker.execute();
    }

    protected void proceed() {
        if (credentialsProvided && dbWorkDone) {
            System.err.println("Continuing ");
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException,
            UnsupportedLookAndFeelException {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                new TestThreadJoin().initUI();
            }
        });
    }
}

【讨论】:

  • 我会接受这个解决方案。它是完整的,看起来它对我有用。即使没有,它的一些衍生物也能胜任。
【解决方案2】:

可以使用 SwingWorker 来完成。 将此 ActionListener 添加到您的 JButton。还为“标签”提供 JLabel 字段,为“进度条”提供 JProgressBar 字段。

java.awt.event.ActionListener al = new java.awt.event.ActionListener() {
    public void actionPerformed(java.awt.event.ActionEvent evt) {
        SwingWorker swingWorker = new SwingWorker() {

            @Override
            protected Object doInBackground() throws Exception {

                // SIMULATE DB WORK
                for (int i = 0; i < 10; i++) {
                    synchronized (this) {
                        this.wait(300);
                    }
                    publish(10 * i);
                }
                publish(100);

                return 0;
            }

            @Override
            protected void done() {
                label.setText("Work complete");
                button.setEnable(true);
            }

            @Override
            protected void process(List chunks) {
                for (Object object : chunks) {
                    Integer progress = (Integer) object;
                    progressBar.setValue(progress);
                }
            }

        };

        button.setEnable(false);
        swingWorker.execute();
    }
}

由于数据库工作可能无法量化(可测量),请进行以下更改:

  • 在“swingWorker.execute()”行上方插入“progressbar.setIndeterminate(true)”,
  • 在“label.setText()”行上方插入“progressbar.setIndeterminate(false)”,
  • 删除 'publish(int)' 调用。

【讨论】:

  • 问题是登录过程的另一半是在另一个类中完成的。从 swing worker 我真的不知道用户是否已经点击了登录按钮,而从其他类我真的无法判断 swing worker 是否完成了它的工作(第 1 阶段登录)或说“停止第 2 阶段登录直到摇摆工人完成了”...
  • 如果将 ActionListener 附加到 Login 按钮,您将知道按下按钮的那一刻,然后执行 swingWorker。在 swingWorker.doInBackground() 中,执行登录调用,例如Controller.login(姓名,密码)。阅读“MVC 模式”以了解更多信息。
【解决方案3】:

我真的认为你应该尝试一下 SwingWorker,方法是:

  • 当用户点击登录时,启动swing worker(new Swingworker<...>().execute();)
  • doInBackground()中,先调用pusblih()调用process()来禁用按钮
  • 然后在doInBackground()中等待后台线程
  • done() 中更新 UI

这样您就不会冻结 EDT...

另一种选择是运行您自己的“mini-swingworker”:

loginButton.setEnabled(false);  // assuming this runs in the EDT
new Thread(new Runnable() {
  public void run() {
    myOtherBgThread.join();  // here we wait for the BG thread
    SwingUtilities.invokeLater(new Runnable() {
      // go back into the EDT
      // update the UI here...
    }
  }
}.start();

希望对你有帮助,问候

【讨论】:

  • 问题是登录过程的另一半是在另一个类中完成的。从swing worker我真的不知道用户是否已经点击了登录按钮(或者在密码字段中按下了“Enter”!),从另一个类我真的不能判断swing worker是否已经完成了它的工作(阶段1 登录)或说“停止第 2 阶段登录,直到摇摆工人完成”...
【解决方案4】:
  1. 最简单的解决方案之一是在初始数据库通信完成之前禁用登录按钮。但这需要一些额外的进度条或消息来表明初始化发生,以免混淆用户。在这种情况下,您可以使用SwingWorker,它会在完成时启用登录按钮。

  2. 另一个解决方案是,当用户按下登录按钮时,应再次将处理委托给(另一个)后台线程,该线程等待初始 DB 通信线程加入。这样 EDT(和 GUI)将不会被阻止。实际上连接到数据库也是一个冗长的操作,所以无论如何都应该在后台线程中完成,即使在禁用登录按钮时,如 1 中所述。

  3. 另一个解决方案是创建一个固定大小为 1 的线程池(例如java.util.concurrent.Executors.newSingleThreadExecutor())并在那里传递所有后台任务。使用单线程执行器任务将按顺序执行。第一个任务是初始数据库通信,另一个是登录任务。

【讨论】:

    【解决方案5】:

    使用 ExecutorService。调用executorService.submit(runnable),其中'runnable' 执行数据库工作。 submit 方法将返回 Future。然后登录后你可以调用future.get(),它会等待完成(或者如果runnable已经完成则立即返回)。

    【讨论】:

    • 我使用了 ScheduledThreadPoolExecutor,提交了那个 db 线程,但是再次调用“Future.get”会阻塞 UI,直到 Executor 完成...
    • 在你调用那个的时候,登录按钮已经被点击了,那么为什么你需要在等待的时候更新UI呢?你不能先更新UI,然后调用get()吗?
    • 如果自动登录阶段 2 立即跟随阶段 1。如果在用户键入时手动登录第 1 阶段,则第 2 阶段在单击“登录”按钮后开始,这可能在第 1 阶段(数据库线程)完成之后或之前。第 2 阶段是共享的 - 手动和自动登录都使用相同的方法。在自动登录的情况下,必须显示不同的 UI,并立即调用阶段 1 和阶段 2。由于“stage1.join”,它可以完成,但是由于“join”,在一切完成后,loding UI 会闪烁。
    • @guest86 在 EDT 上调用 get() 是个坏主意。它将冻结用户界面。 join()、wait()、sleep() 等都不能解决这种情况。