6. Swing应用中的多线程问题
Swing应用运行于多个线程之上。特别是三类线程:
1.起始线程,或主线程,在main()方法中运行,启动GUI的构建和应用的退出;
2.EDT线程;
3.一些处理计算密集型任务及IO的后台线程。
所有的事件处理,绘制和界面刷新代码在单一线程(EDT)中进行,这是为了确保事件处理器在下个处理器启动之前完成执行,且绘制不被事件打断。如果因计算密集型任务造成EDT饥饿,用户界面将会挂起,程序对用户交互无响应。"理想情况下,任何需要花费30-100 ms任务不应该在EDT上运行。否则用户将感觉到输入和UI应答之间的停顿。
再者,所有访问GUI组件的代码应该在EDT上运行,因为这些组件中很多不能确保线程安全,而从相同线程中访问他们能避免多线程问题。
总之,
1.耗时和阻塞IO任务不应在EDT上运行,以避免造成EDT饥饿,进而影响用户交互的事件触发及界面刷新;
2.为了线程安全,Swing组件应仅在EDT上访问。
6.1 javax.Swing.SwingUtilities.invokeLater() 和invokeAndWait()
方法invokeLater(Runnable)和 invokeAndWait(Runnable)协调EDT中的Runnable任务。
为避免主线程(运行main()方法)和EDT之间线程问题,推荐使用javax.swing.SwingUtilities.invokeLater(Runnable)在EDT上创建GUI组件,如:
/** The entry main() method */
public static void main(String args[]) {
// Run the GUI codes on the event-dispatching thread for thread-safety
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("My Swing Application");
frame.setContentPane(new MyMainPanel());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null); // center on screen
frame.setVisible(true); // show it
}
});
}
静态方法SwingUtilities.invokelater()以Runnable对象(作为匿名内部类执行)为参数,并在EDT上制定任务(具体在run()方法中)。可以从任何线程中调用invokeLater()来要求EDT执行特定代码,这些代码可以写在Runnable参数的run()方法中。invokeLater()会立即返回,而不等待EDT执行这些代码。
如果有需要可以使用invokeAndWait(), 这方法会等待EDT执行指定代码才返回。对于applet,推荐(在init())通过invokeAndWait()运行GUI创建代码。这是为了避免init()在GUI创建完成之前退出造成问题。如:
public class SwingTemplateJApplet extends JApplet {
/** init() to setup the GUI components */
@Override
public void init() {
// Run GUI codes in the Event-Dispatching thread for thread safety
try {
// Use invokeAndWait() to ensure that init() exits after GUI construction
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
// Set the content-pane of JApplet to an instance of main JPanel
setContentPane(new SwingTemplateJPanel());
}
});
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
也可用java.awt.EventQueue.invokeLater()代替javax.swing.SwingUtilities.invokeLater(),javax.swing.SwingUtilities.invokeLater()是 java.awt.EventQueue.invokeLater()的进一步封装。
通过之前例子的追踪可以发现如果没有使用SwingUtilities.invokeLater(),EDT在setVisible()方法之后启动。另一方面,invokerLater()启动EDT。
6.2 javax.swing.Timer (JDK 1.2)
如果需要在一定时间后或一段频繁时间间隔内更新组件,使用时间类如 javax.swing.Timer(JDK 1.2)或java.util.Timer(JDK 1. 3)。
对于javax.swing.Timer,阅读"animation using javax.swing.Timer"。
6.3 javax.swing.SwingWorker<T,V> (JDK 1.6)
如之前提到过的,在Swing应用中:
- 计算密集型任务不应在EDT上运行,以避免EDT在处理事件和重绘中出现饥饿;
- 为了线程安全,Swing组件应仅在EDT上访问。
类javax.swing.SwingWorkder<T,V> 帮助管理唯一的EDT和几个后台工作者线程之间的交互。它可以用来协调后台线程中的计算密集型任务并返回EDT中产生的最终结果或中间结果。
定义 SwingWorker 类如:public abstract class SwingWorker<T,V> implements RunnableFuture
SwingWorker<T,V>是带两种类型参数的抽象类:T 指方法doInBackground()和get()最终结果类型,而V指 方法publish()和process()中间结果类型。
RunnableFuture 接口是两个接口的结合:Runnable 和Future。接口Runnable 声明了抽象方法run();而 Future 声明 get(), cancel(), isDone(), 和isCancelled()。
制定后台任务
protected abstract T doInBackground() throws Exception
// Do this task in a background thread
protected void done()
// Executes on the Event-Dispatching thread after the doInBackground() method finishes.
public final T get() throws InterruptedException, ExecutionException
// Waits for doInBackground() to complete and gets the result.
// Calling get() on the Event-Dispatching thread blocks all events, including repaints,
// until the SwingWorker completes.
public final void execute()
// Schedules this SwingWorker for execution on one of the worker thread.
public final boolean cancel(boolean mayInterruptIfRunning)
// Attempts to cancel execution of this task.
public final boolean isDone()
// Returns true if this task has completed (normally or exception)
public final boolean isCancelled()
// Returns true if this task was cancelled before it completed normally
为在工作者线程中制定一项任务,扩展 SwingWorker<T,V>子类并重写:
- doInBackground()指定执行任务,在一个工作者线程中制定并返回类型为T的结果;
- done()方法在 doInBackground()完成后在EDT中运行,用get()方法取得doInBackground()结果 (类型 T)。
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.ExecutionException;
import javax.swing.*;
/** Test SwingWorker on the counter application with a compute-intensive task */
@SuppressWarnings("serial")
public class SwingWorkerCounter extends JPanel {
// For counter
private JTextField tfCount;
private int count = 0;
// For SwingWorker
JButton btnStartWorker; // to start the worker
private JLabel lblWorker; // for displaying the result
/** Constructor to setup the GUI components */
public SwingWorkerCounter () {
setLayout(new FlowLayout());
add(new JLabel("Counter"));
tfCount = new JTextField("0", 10);
tfCount.setEditable(false);
add(tfCount);
JButton btnCount = new JButton("Count");
add(btnCount);
btnCount.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
++count;
tfCount.setText(count + "");
}
});
/** Create a SwingWorker instance to run a compute-intensive task
Final result is String, no intermediate result (Void) */
final SwingWorker<String, Void> worker = new SwingWorker<String, Void>() {
/** Schedule a compute-intensive task in a background thread */
@Override
protected String doInBackground() throws Exception {
// Sum from 1 to a large n
long sum = 0;
for (int number = 1; number < 1000000000; ++number) {
sum += number;
}
return sum + "";
}
/** Run in event-dispatching thread after doInBackground() completes */
@Override
protected void done() {
try {
// Use get() to get the result of doInBackground()
String result = get();
// Display the result in the label (run in EDT)
lblWorker.setText("Result is " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
};
btnStartWorker = new JButton("Start Worker");
add(btnStartWorker);
btnStartWorker.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
worker.execute(); // start the worker thread
lblWorker.setText(" Running...");
btnStartWorker.setEnabled(false); // Each instance of SwingWorker run once
}
});
lblWorker = new JLabel(" Not started...");
add(lblWorker);
}
/** The entry main() method */
public static void main(String[] args) {
// Run the GUI construction in the Event-Dispatching thread for thread-safety
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("SwingWorker Test");
frame.setContentPane(new SwingWorkerCounter());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(300, 150);
frame.setVisible(true);
}
});
}
}
SwingWorker的实例设计只能运行一次且不能重新启动,需要创建新实例重新执行任务。
发布并处理中间结果
不一定在done()中处理最终结果, 如有需要可以publish()并 process()中间结果。
@SafeVarargs
protected final void publish(V... chunks)
// Sends data chunks to the process(java.util.List<V>) method.
// This method shall be called inside the doInBackground() to deliver intermediate results
// for processing on the Event-Dispatching thread inside the process() method.
protected void process(List<V> chunks)
// Receives data chunks from publish() asynchronously on the Event-Dispatching thread.
在doInBackground()中, 用publish(V...) 发布一个或更多类型V中间结果。重写process(List<V>)方法来处理List<V>中公布的结果,process()方法运行在EDT中。
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.ExecutionException;
import javax.swing.*;
/** Test SwingWorker on the counter application with a compute-intensive task */
@SuppressWarnings("serial")
public class SwingWorkerCounterIntermediateResult extends JPanel {
// For counter
private JTextField tfCount;
private int count = 0;
// For SwingWorker
JButton btnStartWorker; // to start the worker
private JLabel lblWorker; // for displaying the result
/** Constructor to setup the GUI components */
public SwingWorkerCounterIntermediateResult () {
setLayout(new FlowLayout());
add(new JLabel("Counter"));
tfCount = new JTextField("0", 10);
tfCount.setEditable(false);
add(tfCount);
JButton btnCount = new JButton("Count");
add(btnCount);
btnCount.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
++count;
tfCount.setText(count + "");
}
});
/** Create a SwingWorker instance to run a compute-intensive task */
final SwingWorker<String, String> worker = new SwingWorker<String, String>() {
/** Schedule a compute-intensive task in a background thread */
@Override
protected String doInBackground() throws Exception {
long sum = 0;
for (int number = 0; number < 10000000; ++number) {
sum += number;
publish(sum + ""); // Send "every" intermediate result to process()
// You might not publish every intermediate result
}
return sum + "";
}
/** Run in event-dispatching thread after doInBackground() completes */
@Override
protected void done() {
try {
// Use get() to get the result of doInBackground()
String finalResult = get();
// Display the result in the label (run in EDT)
lblWorker.setText("Final Result is " + finalResult);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
/** Run in event-dispatching thread to process intermediate results
send from publish(). */
@Override
protected void process(java.util.List<String> chunks) {
// Get the latest result from the list
String latestResult = chunks.get(chunks.size() - 1);
lblWorker.setText("Result is " + latestResult);
}
};
btnStartWorker = new JButton("Start Worker");
add(btnStartWorker);
btnStartWorker.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
worker.execute(); // start the worker thread
lblWorker.setText(" Running...");
btnStartWorker.setEnabled(false); // SwingWorker can only run once
}
});
lblWorker = new JLabel(" Not started...");
add(lblWorker);
}
/** The entry main() method */
public static void main(String[] args) {
// Run the GUI construction in the Event-Dispatching thread for thread-safety
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("SwingWorker Test");
frame.setContentPane(new SwingWorkerCounterIntermediateResult());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(300, 150);
frame.setVisible(true);
}
});
}
}
属性变化事件
doInBackground()发送PropertyChangeEvent给其所有的PropertyChangeListeners 关于边界属性变化。有两个边界属性:"state" 和"progress"。"state"在封装枚举类型SwingWorker.StateValue中定义,StateValue取值为PENDING (SwingWorker 实例创建), START (doInBackground启动)和DONE(doInBackground 完成) 。"progress" 是一个整数型,取值范围从0到100。可以在doInBackground()中 通过setProgress()方法改变progress值来发送PropertyChangeEvent给其所有的PropertyChangeListeners。
例子
在本例 doInBackground()方法内,调用setProgess()来改变progress边界特征值(0-100之间),然后发送PropertyChangeEvent。用SwingWorker定义并注册 PropertyChangeListener , 在进度条显示progress值。事件处理器在EDT中运行。
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.concurrent.ExecutionException;
import javax.swing.*;
/** Test SwingWorker on the counter application with a compute-intensive task */
@SuppressWarnings("serial")
public class SwingWorkerCounterProgress extends JPanel {
// For counter
private JTextField tfCount;
private int count = 0;
// For SwingWorker
JButton btnStartWorker; // to start the worker
private JLabel lblWorker; // for displaying the result
JProgressBar pbWorker; // progress bar for the worker task
/** Constructor to setup the GUI components */
public SwingWorkerCounterProgress () {
setLayout(new FlowLayout());
add(new JLabel("Counter"));
tfCount = new JTextField("0", 10);
tfCount.setEditable(false);
add(tfCount);
JButton btnCount = new JButton("Count");
add(btnCount);
btnCount.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
++count;
tfCount.setText(count + "");
}
});
/** Create a SwingWorker instance to run a compute-intensive task */
final SwingWorker<String, String> worker = new SwingWorker<String, String>() {
/** Schedule a compute-intensive task in a background thread */
@Override
protected String doInBackground() throws Exception {
long sum = 0;
int maxNumber = 10000000;
for (int number = 0; number < maxNumber; ++number) {
sum += number;
publish(sum + ""); // send intermediate result to process()
// Fire PropertyChangeEvent for the bound-property "progress"
setProgress(100 * (number + 1) / maxNumber);
}
return sum + "";
}
/** Run in event-dispatching thread after doInBackground() completes */
@Override
protected void done() {
try {
// Use get() to get the result of doInBackground()
String finalResult = get();
// Display the result in the label (run in EDT)
lblWorker.setText("Final Result is " + finalResult);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
/** Run in event-dispatching thread to process intermediate results
send from publish(). */
@Override
protected void process(java.util.List<String> chunks) {
// Get the latest result from the list
String latestResult = chunks.get(chunks.size() - 1);
lblWorker.setText("Result is " + latestResult);
}
};
/** Event handler for the PropertyChangeEvent of property "progress" */
worker.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals("progress")) { // check the property name
pbWorker.setValue((Integer)evt.getNewValue()); // update progress bar
}
}
});
btnStartWorker = new JButton("Start Worker");
add(btnStartWorker);
btnStartWorker.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
worker.execute(); // start the worker thread
lblWorker.setText(" Running...");
btnStartWorker.setEnabled(false); // SwingWorker can only run once
}
});
lblWorker = new JLabel(" Not started...");
add(lblWorker);
pbWorker = new JProgressBar();
add(pbWorker);
}
/** The entry main() method */
public static void main(String[] args) {
// Run the GUI construction in the Event-Dispatching thread for thread-safety
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("SwingWorker Test");
frame.setContentPane(new SwingWorkerCounterProgress());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(300, 150);
frame.setVisible(true);
}
});
}
}
6.4 总结
线程对构建响应性GUI很有必要,有一些应该使用新线程的典型情形:
- 在主线程中创建一新线程来处理耗时初始化任务(如:磁盘I/O)以便GUI加载更快;
- 在EDT中创建一新线程来处理耗时任务(在事件处理器中)以便GUI保持响应;
- 用计时器处理重复任务,在一定时间间隔或延时一段时间执行;
- 如果操作需要等待其他线程或程序的信息,创建一个新线程。
此外,计算密集型线程必须协作并将控制交与其他线程。
7. Thread Pool, Executor, Callable/Future (JDK 1.5)
线程池支持类在JDK1.5 java.lang.concurrent包中引入:
7.1 线程池(Thread Pool)
线程池是一个能执行任务线程的管理集合。当使用线程池执行大量任务时,效率会因线程循环执行任务而提高,这会降低每个任务调用开销。
可以实现接口ExecutorService来使用线程池,如ThreadPoolExecutor 或ScheduledThreadPoolExecutor,而在Executors 类中提供了更多便捷工厂方法,列举如下:
-
Executors.newSingleThreadExecutor()
: 创建单一后台线程; -
Executors.newFixedThreadPool(int numThreads)
: 创建固定大小的线程池; -
Executors.newCachedThreadPool()
: 创建带自动线程回收机制的无界线程池。
使用线程池的步骤:
- 写一个实现Runnable接口的工作者线程类,run()方法制定运行线程的行为;
- 用Executors 类提供的工厂方法创建线程池(ExecutorService),该线程池可以只有一个线程,固定数量线程或无限数量线程;
- 创建工作者线程类实例。用线程池的方法execute(Runnable r)来添加Runnable 任务到线程池,该任务将被协调并在有空闲线程时得到执行。
7.2 接口java.util.concurrent.Executor
Executor对象能执行提交的Runnable 任务,该接口声明了下面抽象方法:
public void execute(Runnable r)
在未来某个时候执行给定任务,任务因Executor执行情况可能在新线程,线程池或当前线程中执行。
7.3 接口java.util.concurrent.ExecutorService
接口ExecutorService 声明很多抽象方法,其中比较重要的有:
public void shutdown();
// Initiates an orderly shutdown of the thread pool.
// The previously executed/submitted tasks are allowed to complete,
// but no new tasks will be scheduled.
public <T> Future<T> submit(Callable<T> task);
// Submit or schedule the callable task for execution, which returns a Future object.
7.4 类java.util.concurrent.Executors
类Executors 提供创建Executor对象的工厂方法。如:
static ExecutorService newSingleThreadExecutor()
static ExecutorService newFixedThreadPool(int nThreads)
static ExecutorService newCachedThreadPool()
static ScheduledExecutorService newSingleThreadScheduledExecutor()
static ScheduledExecutorService newScheduledThreadPool(int size)
public class WorkerThread implements Runnable {
private int workerNumber;
WorkerThread(int workerNumber) {
this.workerNumber = workerNumber;
}
public void run() {
// The thread simply prints 1 to 5
for (int i = 1; i <= 5; ++i) {
System.out.printf("Worker %d: %d\n", workerNumber, i);
try {
// sleep for 0 to 0.5 second
Thread.sleep((int)(Math.random() * 500));
} catch (InterruptedException e) {}
}
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
int numWorkers = Integer.parseInt(args[0]);
int threadPoolSize = Integer.parseInt(args[1]);
ExecutorService pool =
Executors.newFixedThreadPool(threadPoolSize);
WorkerThread[] workers = new WorkerThread[numWorkers];
for (int i = 0; i < numWorkers; ++i) {
workers[i] = new WorkerThread(i+1);
pool.execute(workers[i]);
}
pool.shutdown();
}
}
//2 threads, 5 workers
> java ThreadPoolTest 5 2
Worker 1: 1
Worker 2: 1
Worker 1: 2
Worker 1: 3
Worker 2: 2
Worker 1: 4
Worker 2: 3
Worker 1: 5
Worker 3: 1
Worker 3: 2
Worker 2: 4
Worker 3: 3
Worker 2: 5
Worker 3: 4
Worker 3: 5
Worker 4: 1
Worker 4: 2
Worker 5: 1
Worker 4: 3
Worker 5: 2
Worker 5: 3
Worker 4: 4
Worker 5: 4
Worker 5: 5
Worker 4: 5
首先安排工作者1和2用线程池中的两个线程执行,接着是工作者3,4和5。占用线程任务完成后,线程返回线程池,然后安排另外一个任务并开始执行。
可以调用pool.shutdown()来关闭线程池中所有线程。
7.5 接口java.util.concurrent.Callable<V> 和Future<V>
Callable 类似于Runnable,不同的是 Callable提供方法返回结果或Exception到相应的线程中。Callable声明了抽象方法call()(不同于Runnable中的run())。
public V call() // Call() returns a result of type <V>, or throws an exception if unable to do so.
在线程池中,使用submit(Callable r)而非execute(Runnable r),这会返回一个Future<V> 对象(在ExecutorService接口中声明)。当需要结果时,可以对Future对象调用get()方法加以获取。结果一旦完成便会返回,否则当前线程一直阻塞直到获取结果。
接口Future<V> 声明了下面的抽象方法:
V get() // wait if necessary, retrieve result
V get(long timeout, TimeUnit unit)
boolean cancel(boolean mayInterruptIfRunning)
boolean isCancelled()
boolean isDone() // return true if this task completed
import java.util.concurrent.Callable;
public class CallableWorkerThread implements Callable<String> {
private int workerNumber;
CallableWorkerThread(int workerNumber) {
this.workerNumber = workerNumber;
}
public String call() { // use call() instead of run()
for (int i = 1; i <= 5; ++i) { // just print 1 to 5
System.out.printf("Worker %d: %d\n", workerNumber, i);
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {}
}
return "worker " + workerNumber;
}
}
import java.util.concurrent.*;
public class CallableThreadPoolTest {
public static void main(String[] args) {
int numWorkers = Integer.parseInt(args[0]);
ExecutorService pool = Executors.newCachedThreadPool();
CallableWorkerThread workers[] = new CallableWorkerThread[numWorkers];
Future[] futures = new Future[numWorkers];
for (int i = 0; i < numWorkers; ++i) {
workers[i] = new CallableWorkerThread(i + 1);
futures[i] = pool.submit(workers[i]);
}
for (int i = 0; i < numWorkers; ++i) {
try {
System.out.println(futures[i].get() + " ended");
} catch (InterruptedException ex) {
ex.printStackTrace();
} catch (ExecutionException ex) {
ex.printStackTrace();
}
}
}
}
三个工作者线程的输出:
> java CallableThreadPoolTest 3
Worker 1: 1
Worker 3: 1
Worker 2: 1
Worker 3: 2
Worker 1: 2
Worker 2: 2
Worker 2: 3
Worker 2: 4
Worker 2: 5
Worker 1: 3
Worker 3: 3
Worker 3: 4
Worker 3: 5
Worker 1: 4
Worker 1: 5
worker 1 ended
worker 2 ended
worker 3 ended
7.6 JDK 1.5中其他新线程特性
- Lock
- ReadWriteLock
- Semaphores
- Atomics
- Blocking Queue
更多参考文献及资源
- Swing Tutorial's "Concurrency in Swing".
- John O'Conner, "Improve Application Performance With SwingWorker in Java SE 6" @ http://java.sun.com/developer/technicalArticles/javase/swingworker/.
翻译自:
https://www3.ntu.edu.sg/home/ehchua/programming/java/j5e_multithreading.html