Java中的巧妙异步重绘

我有一个用例来自GUI问题,我想提交给你的睿智。

用例

我有一个GUI,根据用户在GUI中设置的一些参数显示计算结果。 例如,当用户移动滑块时,会触发几个事件,这些事件都会触发新的计算。 当用户将滑块值从A调整为B时,会触发数十个事件。

但是计算可能需要几秒钟,而滑块调整可以每隔几百毫秒触发一次事件。

如何编写一个能够监听这些事件的正确线程,并对它们进行过滤以使结果的重绘更加生动? 理想情况下,你会喜欢这样的东西

  • 收到第一次更改事件后立即开始新的计算;
  • 如果收到新事件则取消第一次计算,并用新参数开始新计算;
  • 但要确保最后一个事件不会丢失,因为最后完成的计算需要是具有最后更新参数的计算。

我试过的

我的一个朋友(A. Cardona)提出了一种更新程序线程的低级方法,可以防止太多事件触发计算。 我在这里复制粘贴(GPL):

他把它放在一个扩展Thread的类中:

public void doUpdate() { if (isInterrupted()) return; synchronized (this) { request++; notify(); } } public void quit() { interrupt(); synchronized (this) { notify(); } } public void run() { while (!isInterrupted()) { try { final long r; synchronized (this) { r = request; } // Call refreshable update from this thread if (r > 0) refresh(); // Will trigger re-computation synchronized (this) { if (r == request) { request = 0; // reset wait(); } // else loop through to update again } } catch (Exception e) { e.printStackTrace(); } } } public void refresh() { // Execute computation and paint it ... } 

每当GUI发送一个事件表明参数已被更改时,我们调用updater.doUpdate() 。 这会导致方法refresh()被调用得更少。 但我无法控制这一点。

其他方式?

我想知道是否有另一种方法可以使用jaca.concurrent类。 但我无法在Executors框架中排序我应该开始的那个。

你们有没有一些类似用例的经验?

谢谢

如果您正在使用Swing ,则SwingWorker为此提供function,您无需自己处理线程池。

为每个请求触发SwingWorker 。 如果有新请求进入且工作人员没有完成,您可以cancel()它,然后启动一个新的SwingWorker 。 关于另一张海报所说的内容,我认为publish()process()并不是你想要的(尽管它们也非常有用),因为它们适用于工作人员可能比GUI可以处理它。

 ThingyWorker worker; public void actionPerformed(ActionEvent e) { if( worker != null ) worker.cancel(); worker = new ThingyWorker(); worker.execute(); } class ThingyWorker extends SwingWorker { @Override protected YOURCLASS doInBackground() throws Exception { return doSomeComputation(); // Should be interruptible } @Override protected void done() { worker = null; // Reset the reference to worker YOURCLASS data; try { data = get(); } catch (Exception e) { // May be InterruptedException or ExecutionException e.printStackTrace(); return; } // Do something with data } } 

action和done()方法都在同一个线程上执行,因此它们可以有效地检查对是否存在现有worker的引用。

请注意,实际上这是允许GUI取消现有操作的相同操作,除非在触发新请求时自动完成取消。

我将通过使用队列在GUI和控件之间提供进一步的断开连接。

如果在两个进程之间使用BlockingQueue 。 每当控件更改时,您都可以将新设置发布到队列中。

您的图形组件可以随时读取队列并对到达的事件进行操作或根据需要丢弃它们。

我会查看SwingWorker.publish()( http://docs.oracle.com/javase/6/docs/api/javax/swing/SwingWorker.html

Publish允许SwingWorker对象的后台线程调用process()方法,但不是每个publish()调用都会调用process()。 如果在process()返回之前进行多个进程调用并且可以再次调用,则SwingWorker将用于多个发布调用的参数连接到一个进程调用。

我有一个进度对话框,显示正在处理的文件; 文件的处理速度比UI跟不上它们的速度要快,我不希望处理速度变慢以显示文件名; 我使用了这个并且进程只显示发送到process()的最终文件名; 在这种情况下,我想要的只是向用户指出当前处理的位置,他们无论如何都不会读取所有文件名。 我的用户界面非常顺利。

看一下javax.swing.SwingWorker(Java JDK中的源代码)的实现,重点关注两种方法之间的握手: 发布处理

这些不会直接适用于您的问题 – 但是它们演示了如何将更新排队(发布)到工作线程,然后在工作线程(进程)中为它们提供服务。

由于您只需要上次工作请求,因此您甚至不需要为您的情况设置队列:仅保留上次工作请求。 在一段时间(1秒)内“上次请求”的样本,以避免每1秒钟停止/重启多次,如果它已经改变那么停止工作并重新启动。


您不希望按原样使用发布 / 进程的原因是该进程始终在Swing Event Dispatch Thread上运行 – 完全不适合长时间运行的计算。

这里的关键是你希望能够取消正在进行的计算。 计算必须经常检查条件以查看是否需要中止。

 volatile Param newParam; Result compute(Param param) { loop compute a small sub problem if(newParam!=null) // abort return null; return result } 

将param从事件线程切换到计算线程

 synchronized void put(Param param) // invoked by event thread newParam = param; notify(); synchronized Param take() while(newParam==null) wait(); Param param = newParam; newParam=null; return param; 

而计算线程呢

 public void run() while(true) Param param = take(); Result result = compute(param); if(result!=null) paint result in event thread