您的位置:首页 > 编程语言 > Java开发

Java核心技术卷I:基础知识(原书第8版):14.11 线程与Swing

2014-10-31 14:23 351 查看
铁文整理

14.11
线程与Swing

在有关本章的介绍里已经提到,在程序中使用线程的理由之一是提高程序的响应性能。当程序需要做某些耗时的工作时,应该启动另一个工作器线程而不是阻塞用户接口。

但是,必须认真考虑工作器线程在做什么,因为这或许令人惊讶,Swing不是线程安全的。如果你试图在多个线程中操纵用户界面的元素。那么用户界面可能崩溃。

要了解这一问题,运行例14-14的测试程序。当你点击Bad按钮时,一个新的线程将启动,它的run方法操作一个组合框,随机地添加值和删除值。

public
void
run() {
try {
while (true) {
int i = Math.abs(generator.nextInt());
if (i % 2 == 0)
combo.insertItemAt(i, 0);
else
if
(combo.getItemCount() > 0)
combo.removeItemAt(i %
combo.getItemCount());
Thread.sleep(1);
}
} catch (InterruptedException e) {
}
}
试试看。点击Bad按钮。点击几次组合框,移动滚动条,移动窗口,再次点击Bad按钮,不断点击组合框。最终,你会看到一个异常报告(见图14-9)。

发生了什么?当把一个元素插入组合框时,组合框将产生一个事件来更新显示。然后,显示代码开始运行,读取组合框的当前大小并准备显示这个值{。但是,工作器线程保持运行,有时候会造成组合框中值的数目减少。显示代码认为组合框中的值比实际的数量多,于是会访问不存在的值,触发ArrayIndexOutOfBounds异常。

在显示时对组合框加锁可以避免这种情况出现。但是,Swing的设计者决定不再付出更多的努力实现Swing线程安全,有两个原因。首先,同步需要时间,而且,已经没有人想要降低Swing的速度。更重要的是,Swing小组调査了其他小组在线程安全的用户界面工具包方面的经验。他们的发现并不令人鼓舞。使用线程安全包的程序员被同步命令搞昏了头,常常编写出容易造成死锁的程序。

14.11.1
运行耗时的任务

将线程与Swing一起使用时,必须遵循两个简单的原则。

如果一个动作需要花费很长时间,在一个独立的工作器线程中做这件事不要在事件分配线程中做。

除了事件分配线程,不要在任何线程中接触Swing组件。

制定第一条规则的理由易于理解。如果花很多时间在事件分配线程上应用程序像“死了”一样,因为它不响应任何事件。特别是,事件分配线程应该永远不要进行input/output调用,这有可能会阻塞,并且应该永远不要调用sleep。(如果需要等待指定的时间,使用定时器事件。)

第二条规则在Swing编程中通常称为单一线程规则(single-thread rule)。我们在后面的内容中进一步讨论。

这两条规则看起来彼此冲突。假定要启动一个独立的线程运行一个耗时的任务。线程工作的时候,通常需要更新用户界面中指示执行的进度。任务完成的时候,要再—次更新GUI界面。但是,不能从自己的线程接触Swing组件。例如,如果要更新进度条或标签文本,不能从线程中设置它的值。

要解决这一问题,在任何线程中,可以使用两种有效的方法向事件队列添加任意的动作。例如,假定想在一个线程中周期性地更新标签来表明进度。不可以从自己的线程中调用label.setText,而应该使用EventQueue类的invokeLater方法和invokeAndWait方法使所调用的方法在事件分配线程中执行。

应该将Swing代码放置到实现Runnable接口的类的run方法中。然后,创建该类的一个对象,将其传递给静态的invokeLater或invokeAndWait方法。例如,下面是如何更新标签内容的代码:

EventQueue.invokeLater(new Runnable() {
public
void
run() {
label.setText(percentage +
"% complete");
}
});
当事件放入事件队列时,invokeLater方法立即返回,而run方法被异步执行。invokeAndWait方法等待直到run方法确被实执行过为止。

在更新进度标签时,invokeLater方法更适宜。用户更希望让工作器线程有更快完成工作而不是得到更加精确的进度指示器。

这两种方法都是在事件分配线程中执行run方法。没有新的线程被创建。

例14-14演示了如何使用invokeLater方法安全地修改组合框的内容,如果点击Good按钮,线程插入或移除数字。但是,实际的修改是发生在事件分配线程中。

例14-14 SwingThreadTest.java
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

/**
* This program demonstrates that a thread that runs in parallel with the event
* dispatch thread can cause errors in Swing components.
*
* @version 1.23 2007-05-17
* @author Cay Horstmann
*/
public
class
SwingThreadTest {
public
static void
main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public
void
run() {
SwingThreadFrame frame = new SwingThreadFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});
}
}

/**
* This frame has two buttons to fill a combo box from a separate thread. The
* "Good" button uses the event queue, the "Bad" button modifies the combo box
* directly.
*/
class SwingThreadFrame
extends JFrame {
public SwingThreadFrame() {
setTitle("SwingThreadTest");

final JComboBox combo =
new JComboBox();
combo.insertItemAt(Integer.MAX_VALUE, 0);
combo.setPrototypeDisplayValue(combo.getItemAt(0));
combo.setSelectedIndex(0);

JPanel panel = new JPanel();

JButton goodButton = new JButton("Good");
goodButton.addActionListener(new ActionListener() {
public
void
actionPerformed(ActionEvent event) {
new Thread(new GoodWorkerRunnable(combo)).start();
}
});
panel.add(goodButton);
JButton badButton = new JButton("Bad");
badButton.addActionListener(new ActionListener() {
public
void
actionPerformed(ActionEvent event) {
new Thread(new BadWorkerRunnable(combo)).start();
}
});
panel.add(badButton);

panel.add(combo);
add(panel);
pack();
}
}

/**
* This runnable modifies a combo box by randomly adding and removing numbers.
* This can result in errors because the combo box methods are not synchronized
* and both the worker thread and the event dispatch thread access the combo
* box.
*/
class BadWorkerRunnable
implements Runnable {
public BadWorkerRunnable(JComboBox aCombo) {
combo = aCombo;
generator =
new
Random();
}

public
void
run() {
try {
while (true) {
int i = Math.abs(generator.nextInt());
if (i % 2 == 0)
combo.insertItemAt(i, 0);
else
if
(combo.getItemCount() > 0)
combo.removeItemAt(i %
combo.getItemCount());
Thread.sleep(1);
}
} catch (InterruptedException e) {
}
}

private JComboBox
combo;
private Random
generator;
}

/**
* This runnable modifies a combo box by randomly adding and removing numbers.
* In order to ensure that the combo box is not corrupted, the editing
* operations are forwarded to the event dispatch thread.
*/
class GoodWorkerRunnable
implements Runnable {
public GoodWorkerRunnable(JComboBox aCombo) {
combo = aCombo;
generator =
new
Random();
}

public
void
run() {
try {
while (true) {
EventQueue.invokeLater(new Runnable() {
public
void
run() {
int i = Math.abs(generator.nextInt());
if (i % 2 == 0)
combo.insertItemAt(i, 0);
else
if
(combo.getItemCount() > 0)
combo.removeItemAt(i %
combo.getItemCount());
}
});
Thread.sleep(1);
}
} catch (InterruptedException e) {
}
}

private JComboBox
combo;
private Random
generator;
}

API:java.awt.EventQueue 1.1

static void invokeLater(Runnable runnable) 1.2:在待处理的线程被处理之后,让runnable对象的run方法在事件分配线程中执行。

static void invokeAndWait(Runnable runnable) 1.2:在待处理的线程被处理之后,让runnable对象的run方法在事件分配线程中执行。该调用会阻塞,直到run方法终止。

static boolean isDispatchThread() 1.2:如果执行这一方法的线程是事件分配线程,返回true。

14.11.2
使用Swing工作器

当用户发布一条处理过程很耗时的命令时,你可能打算启动一个新的线程来完成这个工作。如同上一节介绍的那样,线程应该使用EvemQueue.invokeLater方法来更新用户界面。

有人已经创建了很方便的类,让人们轻松地完成这样的任务,并且,这些类中的一个已经编入Java SE 6。本节我们介绍SwingWorker类。

例14-15中的程序有加载文本文件的命令和取消加载过程的命令。应该用一个长的文件来测试这个程序,例如The Count of Monte Cristo的全文,它在本书的附赠代码的gutenberg目录下。该文件在一个单独的线程中加载。在读取文件的过程中,Open菜单项被禁用,Cancel菜单项为可用(见图14-10)。读取每一行后,状态条中的线性计数器被更新。读取过程完成之后,Open菜单项重新变为可用,Cancel项被禁用,状态行文本置为Done。

这个例子展示了后台任务的典型UI活动:

在每一个工作单位完成之后,更新UI来显示进度。

整个工作完成之后,对UI做最后的更新。

SwingWorker类使得实现这一任务轻而易举,覆盖doInBackground方法来完成耗时的工作,不时地调用publish来报告工作进度。这一方法在工作器线程中执行。publish方法使得process方法在事件分配线程中执行来处理进度数据。当工作完成时,done方法在事件分配线程中被调用以便完成UI的更新。

每当要在工作器线程中做一些工作时,构建一个新的工作器(每一个工作器对象只能被使用一次)。然后调用execute方法。典型的方式是在事件分配线程中调用execute,但没有这样的需求。

假定工作器产生某种类型的结果;因此,SwingWorker<T, V>实现Future<T>。这一结果可以通过Future接口的get方法获得。由于get方法阻塞直到结果成为可用,因此不要在调用execute之后马上调用它,只在已经知道工作完成时调用它,是最为明智的。典型地,可以从done方法调用get。(有时,没有调用get的需求,处理进度数据就是你所需要的。)

中间的进度数据以及最终的结果可以是任何类型。SwingWorker类有3种类型作为类型参数。SwingWorker<T, V>产生类型为T的结果以及类型为V的进度数据。

要取消正在进行的工作,使用Future接口的cancel方法。当该工作被取消的时候,get方法抛出CancellationException异常。

正如前面已经提到的,工作器线程对publish的调用会导致在事件分配线程上的process的调用。为了提高效率,几个对publish的调用结果,可用对process的一次调用成批处理。process方法接收一个包含所有中间结果的列表<V>。

把这一机制用于读取文本文件的工作中。正如所看到的,JTextArea相当慢。在一个长的文本文件(比如,The Count of Monte Cristo)中追加行会花费相当可观的时间。

为了向用户展示进度,要在状态行中显示读入的行数。因此,进度数据包含当前行号以及文本的当前行。将它们打包到一个普通的内部类中:

private
class
ProgressData {
public
int
nunber;
public String
line;
}
最后的结果是已经读入StringBuilder的文本。因此,需要一个SwingWorkder<StringBuilder, ProgressData>。

在doInBackground方法中,读取一个文件,每次一行,在读取每一行之后,调用publish方法发布行号和当前行的文本。

@Override
public StringBuilder doInBackground()
throws IOException, InterruptedException {
int lineNumber = 0;
Scanner in = new Scanner(new FileInputStream(file));
while (in.hasNextLine()) {
String line = in.nextLine();
lineNumber++;
text.append(line);
text.append("\n");
ProgressData data = new ProgressData();
data.number = lineNumber;
data.line = line;
publish(data);
Thread.sleep(1);
// to test cancellation; no need to do this in your programs
}
return
text;
}

在读取每一行之后休眠1毫秒,以便不使用重读就可以检测取消动作,但是,不要使用休眠来减慢程序的执行速度。如果对这一行加注解,会发现The Count of Monte Cristo的加载相当快,只有几批用户接口更新。

注释:从工作器线程来更新文本区可以使这个程序的处理相当顺畅,但是,对大多数Swing组件来说不可能做到这一点。这里,给出一种通用的方法,其中所有组件的更新都出现在事件分配线程中。

在这个process方法中,忽略除最后一行行号之外的所有行号,然后,我们把所有的行拼接在一起用于文本区的一次更新。

@Override
public
void
process(List<ProgressData> data) {
if (isCancelled())
return;
StringBuilder b = new StringBuilder();
statusLine.setText("" + data.get(data.size() - 1).number);
for (ProgressData d : data) {
b.append(d.line);
b.append("\n");
}
textArea.append(b.toString());
}

在done方法中,文本区被更新为完整的文本,并且Cancel菜单项被禁用。

在Open菜单项的事件监听器中,工作器是如何启动的。这一简单的技术允许人们在保持对用户界面的正常响应的同时,执行耗时的任务。

例14-15 SwingWorkerTest.java
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;

import javax.swing.*;

/**
* This program demonstrates a worker thread that runs a potentially
* time-consuming task.
*
* @version 1.1 2007-05-18
* @author Cay Horstmann
*/
public
class
SwingWorkerTest {
public
static void
main(String[] args)
throws Exception {
EventQueue.invokeLater(new Runnable() {
public
void
run() {
JFrame frame = new SwingWorkerFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});
}
}

/**
* This frame has a text area to show the contents of a text file, a menu to
* open a file and cancel the opening process, and a status line to show the
* file loading progress.
*/
class SwingWorkerFrame
extends JFrame {
public SwingWorkerFrame() {
chooser =
new
JFileChooser();
chooser.setCurrentDirectory(new File("."));

textArea =
new
JTextArea();
add(new JScrollPane(textArea));
setSize(DEFAULT_WIDTH,
DEFAULT_HEIGHT);

statusLine =
new
JLabel(" ");
add(statusLine, BorderLayout.SOUTH);

JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);

JMenu menu = new JMenu("File");
menuBar.add(menu);

openItem =
new
JMenuItem("Open");
menu.add(openItem);
openItem.addActionListener(new ActionListener() {
public
void
actionPerformed(ActionEvent event) {
// show file chooser dialog
int result =
chooser.showOpenDialog(null);

// if file selected, set it as icon of the label
if (result == JFileChooser.APPROVE_OPTION) {
textArea.setText("");
openItem.setEnabled(false);
textReader =
new TextReader(chooser.getSelectedFile());
textReader.execute();
cancelItem.setEnabled(true);
}
}
});

cancelItem =
new
JMenuItem("Cancel");
menu.add(cancelItem);
cancelItem.setEnabled(false);
cancelItem.addActionListener(new ActionListener() {
public
void
actionPerformed(ActionEvent event) {
textReader.cancel(true);
}
});
}

private
class
ProgressData {
public
int

number;
public String
line;
}

private
class
TextReader extends SwingWorker<StringBuilder, ProgressData> {
public TextReader(File file) {
this.file = file;
}

// the following method executes in the worker thread; it doesn't touch Swing components

@Override
public StringBuilder doInBackground()
throws IOException, InterruptedException {
int lineNumber = 0;
Scanner in = new Scanner(new FileInputStream(file));
while (in.hasNextLine()) {
String line = in.nextLine();
lineNumber++;
text.append(line);
text.append("\n");
ProgressData data = new ProgressData();
data.number = lineNumber;
data.line = line;
publish(data);
Thread.sleep(1);
// to test cancellation; no need to do this in your programs
}
return
text;
}

// the following methods execute in the event dispatch thread

@Override
public
void
process(List<ProgressData> data) {
if (isCancelled())
return;
StringBuilder b = new StringBuilder();
statusLine.setText("" + data.get(data.size() - 1).number);
for (ProgressData d : data) {
b.append(d.line);
b.append("\n");
}
textArea.append(b.toString());
}

@Override
public
void
done() {
try {
StringBuilder result = get();
textArea.setText(result.toString());
statusLine.setText("Done");
} catch (InterruptedException ex) {
} catch (CancellationException ex) {
textArea.setText("");
statusLine.setText("Cancelled");
} catch (ExecutionException ex) {
statusLine.setText("" + ex.getCause());
}

cancelItem.setEnabled(false);
openItem.setEnabled(true);
}

private File
file;
private StringBuilder
text = new StringBuilder();
};

private JFileChooser
chooser;
private JTextArea
textArea;
private JLabel
statusLine;
private JMenuItem
openItem;
private JMenuItem
cancelItem;
private SwingWorker<StringBuilder, ProgressData>
textReader;

public
static final
int
DEFAULT_WIDTH = 450;
public
static final
int
DEFAULT_HEIGHT = 350;
}

API:javax.swing.SwingWorker<T, V> 6

abstract T doInBackground():覆盖这一方法来执行后台的任务并返回这一工作的结果。

void process(List<V> data):覆盖这一方法来处理事件分配线程中的中间进度数据。

void publish(V... data):传递中间进度数据到事件分配线程。从doInBackground调用这一方法。

void execute():为工作器线程的执行预定这个工作器。

SwingWorker.StateValue getState():得到这个工作器线程的状态,值为PENDING、STARTED或DONE之一。

14.11.3
单一线程规则

每一个Java应用程序都开始于主线程中的main方法。在Swing程序中,main方法的生命期是很短的。它在事件分配线程中规划用户界面的构造然后退出,在用户界面构造之后,事件分配线程会处理事件通知,例如调用actionPerformed或paintComponent。其他线程在后台运行,例如将事件放入事件队列的进程,但是那些线程对应用程序员是不可见的。

本章前面介绍了单一线程规则:除了事件分配线程,不要在任何线程中接触Swing组件。本节进一步研究此规则。

对于单一线程规则存在一些例外情况。

可在任一个线程里添加或移除事件监听器。当然该监听器的方法会在事件分配线程中被触发。

只有很少的Swing方法是线程安全的。在API中用这样的句子特别标明:“尽管大多数Swing方法不是线程安全的,但这一方法是。”在这些线程安全的方法中最有用的是:

JTextComponent.setText

JTextArea.insert

JTextArea.append

JTextArea.replaceRange

JComponent.repaint

JComponent.revalidate

注释:在本书中多次使用repaint方法,但是,revalidate方法不怎么常见。这样做的目的是在内容改变之后强制执行组件布局。传统的AWT有一个validate方法强制执行组件布局。对于Swing组件,应该调用revalidate方法。(但是,要强制执行JFrame的布局,仍然要调用validate方法,因为JFrame是一个Component不是一个JComponent。)

历史上,单一线程规则是更加随意的。任何线程都可以构建组件,设置优先级,将它们添加到容器中,只要这些组件没有一个是已经被实现的(realized)。如果组件可以接收paint事件或validation事件,组件被实现。一旦调用组件的setVisible(true)或pack(!)方法或者组件已经被添加到已经被实现的容器中,就出现这样的情况。

单一线程规则的这一版本是便利的,它允许在main方法中创建GUI,然后,在应用程序的顶层框架调用setVisible(true)。在事件分配线程上没有令人讨厌的Runnable的安排。

遗憾的是,一些组件的实现者没有注意原来的单一线程规则的微妙之处。他们在事件分配线程启动活动,而没有检査组件是否是被实现的。例如,如果在JTextComponent上调用setSelectionStart或setSelectionEnd,在事件分配线程中安排了一个插入符号的移动,即使该组件不是可见的。

检测并定位这些问题可能会好些,但是Swing的设计者没有走这条轻松的路。他们认定除了使用事件分配线程之外,从任何其他线程访问组件永远都是不安全的。因此,你需要在事件分配线程构建用户界面,像程序示例中那样调用EventQueue.invokeLater。

当然,有不少程序使用旧版的单一线程规则。在主线程初始化用户界面。那些程序有一定的风险,某些用户界面的初始化会引起事件分配线程的动作与主线程的动作发生冲突,如同我们在第7章讲到的,不要让自己成为少数不幸的人之一,为时有时无的线程bug烦恼并花费时间。因此,一定要遵循严谨的单一线程规则。

现在读者已经读到《Java核心技术卷I》的末尾。这一卷涵盖了Java程序设计语言的基础知识以及大多数编程项目所需要的标准库中的部分内容。希望读者在学习Java基础知识的过程中感到偷快并得到了有用的信息。有关高级知识内容,如网络、高级的AWT/Swing、安全性以及国际化,请阅读卷II。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: