同步器
java.util.concurrent 包包含了几个能帮助人们管理相互合作的线程集的类见表 14-5。这 些机制具有为线程之间的共用集结点模式(common rendezvous patterns) 提供的“ 预置功能” ( canned functionality)0 如果有一个相互合作的线程集满足这些行为模式之一, 那么应该直接 重用合适的库类而不要试图提供手工的锁与条件的集合。

信号量
概念上讲,一个信号量管理许多的许可证(permit)。为了通过信号量,线程通过调用 acquire请求许可。其实没有实际的许可对象, 信号量仅维护一个计数。许可的数目是固定 的,由此限制了通过的线程数量。其他线程可以通过调用 release 释放许可。而且,许可不是二 必须由获取它的线程释放。事实上,任何线程都可以释放任意数目的许可,这可能会增加许可数目以至于超出初始数目。
信号量在 1968 年由 Edsger Dijkstra 发明, 作为同步原语(synchronization primitive ) 0 Dijkstra 指出信号量可以被有效地实现, 并且有足够的能力解决许多常见的线程同步问题。
倒计时门栓
一个倒计时门栓(CountDownLatch) 让一个线程集等待直到计数变为 0。倒计时门栓是 一次性的。一旦计数为 0, 就不能再重用了。 一个有用的特例是计数值为 1 的门栓。实现一个只能通过一次的门。线程在门外等候直 到另一个线程将计数器值置为0。
障栅
CyclicBarrier 类实现了一个集结点(rendezvous) 称为障栅(barrier)。考虑大量线程运行 在一次计算的不同部分的情形。当所有部分都准备好时,需要把结果组合在一起。当一个线 程完成了它的那部分任务后, 我们让它运行到障栅处。一旦所有的线程都到达了这个障栅, 障栅就撤销,线程就可以继续运行。
。首先, 构造一个障栅,并给出参与的线程数:
CyclicBarrier barrier = new CydicBarrier(nthreads);
每一个线程做一些工作,完成后在障栅上调用 await :
public void run() {
doWork();
bamer.await();
...
}
await 方法有一个可选的超时参数:
barrier.await(100, TineUnit.MILLISECONDS);
如果任何一个在障栅上等待的线程离开了障栅, 那么障栅就被破坏了(线程可能离开是 因为它调用 await 时设置了超时,或者因为它被中断了) 。在这种情况下,所有其他线程的 await 方法抛出 BrokenBarrierException 异常。那些已经在等待的线程立即终止 await 的调用。
可以提供一个可选的障栅动作(barrier action), 当所有线程到达障栅的时候就会执行这 一动作。
Runnable barrierAction = ...;
CyclicBarrier barrier = new Cyc1icBarrier(nthreads, barrierAction);
该动作可以收集那些单个线程的运行结果。 障栅被称为是循环的(cyclic), 因为可以在所有等待线程被释放后被重用。在这一点上, 有别于 CountDownLatch, CountDownLatch 只能被使用一次。
Phaser 类增加了更大的灵活性,允许改变不同阶段中参与线程的个数。
交换器
当两个线程在同一个数据缓冲区的两个实例上工作的时候, 就可以使用交换器 ( Exchanger) 典型的情况是, 一个线程向缓冲区填人数据, 另一个线程消耗这些数据。当它 们都完成以后,相互交换缓冲区。
同步队列
同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用 SynchronousQueue 的 put 方法时,它会阻塞直到另一个线程调用 take方法为止,反之亦然。与 Exchanger 的情 况不同, 数据仅仅沿一个方向传递,从生产者到消费者。 即使 SynchronousQueue 类实现了 BlockingQueue 接口, 概念上讲, 它依然不是一个队 列。它没有包含任何元素,它的 size方法总是返回 0。
线程与 Swing
当程序需要做某些耗时的工作时,应该启动另一个工作器线程而不是阻塞用户接口。
但是,必须认真考虑工作器线程在做什么,因为这或许令人惊讶,Swing不是线程安全 的。如果你试图在多个线程中操纵用户界面的元素,那么用户界面可能崩溃。 要了解这一问题,运行程序清单 14-13 中的测试程序。当你点击 Bad 按钮时, 一个新的 线程将启动,它的 run方法操作一个组合框,随机地添加值和删除值。
public void run() (
try { while (true) {
int i = Math.abs(generator.nextlnt());
if (i X 2 = 0)
combo.insertltemAt(new Integer(i), 0);
else if (combo.getltemCountO > 0)
combo.renioveIteinAt(i % combo.getltemCountO);
sleep(1);
} catch (InterruptedException e) {}
}
}
试试看。点击 Bad 按钮。点击几次组合框, 移动滚动条, 移动窗口, 再次点击 Bad 按 钮, 不断点击组合框。最终,你会看到一个异常报告(见下图 )。

运行耗时的任务
将线程与 Swing—起使用时, 必须遵循两个简单的原则。
( 1 ) 如果一个动作需要花费很长时间,在一个独立的工作器线程中做这件事不要在事件 分配线程中做。
( 2 ) 除了事件分配线程,不要在任何线程中接触 Swing 组件。
不可以从自己的线程中调用 label.setText,而应该使用 EventQueue类的 invokeLater方法 和 invokeAndWait方法使所调用的方法在事件分配线程中执行。
应该将 Swing 代码放置到实现 Runnable 接口的类的 run方法中。然后,创建该类的一个 对象,将其传递给静态的 invokeLater或 invokeAndWait方法。例如, 下面是如何更新标签内 容的代码:
EventQueue.invokeLater(()->{
label.setText(percentage + "56 complete");
});
当事件放人事件队列时,invokeLater方法立即返回,而run方法被异步执行。invokeAnd Wait方法等待直到 run方法确实被执行过为止。
在更新进度标签时, invokeLater方法更适宜。用户更希望让工作器线程有更快完成工作 而不是得到更加精确的进度指示器。
这两种方法都是在事件分配线程中执行 run 方法。没有新的线程被创建。
程序清单 14-13 演示了如何使用 invokeLater方法安全地修改组合框的内容。如果点击 Good 按钮, 线程插人或移除数字。但是,实际的修改是发生在事件分配线程中。
//程序清单 14-13 swing/SwingThreadTest.java
package swing;
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()
{
JFrame frame = new SwingThreadFrame();
frame.setTitle("SwingThreadTest");
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()
{
final JComboBox<Integer> 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
{
private JComboBox<Integer> combo;
private Random generator;
public BadWorkerRunnable(JComboBox<Integer> 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)
{
}
}
}
/**
* 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
{
private JComboBox<Integer> combo;
private Random generator;
public GoodWorkerRunnable(JComboBox<Integer> 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)
{
}
}
}
使用 Swing 工作线程
SwingWorker 类使后台任务的实现不那么繁琐。 程序清单 14-14 中的程序有加载文本文件的命令和取消加载过程的命令。应该用一个长 的文件来测试这个程序, 例如 The Count of Monte Cristo 的全文, 它在本书的附赠代码的 gutenberg 目录下。该文件在一个单独的线程中加载。在读取文件的过程中, Open 菜单项被 禁用, Cancel 菜单项为可用。读取每一行后,状态条中的线性计数器被更新。 读取过程完成之后, Open 菜单项重新变为可用, Cancel 项被禁用,状态行文本置为 Done。
这个例子展示了后台任务的典型 UI 活动:
•在每一个工作单位完成之后,更新 UI 来 显示进度。
•整个工作完成之后, 对 U丨做最后的更新。
SwingWorker 类使得实现这一任务轻而易 举。 覆盖 doInBackground 方法来完成耗时的工 作, 不时地调用 publish 来报告工作进度。这 一方法在工作器线程中执行。publish 方法使得 process 方法在事件分配线程中执行来处理进度数据。当工作完成时, done方法在事件分配线程中被调用以便完成 UI 的更新。
为了向用户展示进度,要在状态行中显示读入的行数。因此, 进度数据包含当前行号以 及文本的当前行。将它们打包到一个普通的内部类中:
private class ProgressData {
public int number;
public String line;
}
最后的结果是已经读人 StringBuilder 的文本。因此, 需要一个 SwingWorker<StringBuilder, ProgressData〉。
在 doInBackground方法中, 读取一个文件, 每次一行。在读取每一行之后, 调用 publish方法发布行号和当前行的文本。
@Override public StringBuilder doInBackground() throws IOException, InterruptedException {
int lineNumber = 0;
Scanner in = new Scanner(new FilelnputStream(file), "UTF-8");
while (in.hasNextLine()) {
String line = in.nextLine();
lineNumber++; text.append(line).append("\n");
ProgressData data = new ProgressDataO: data.number = lineNumber;
data.line = line; publish(data); Thread.sleep(l ); // to test cancellation; no need to do this in your programs
}
return text;
}
在读取每一行之后休眠 1 毫秒, 以便不使用重读就可以检测取消动作, 但是, 不要使用 休眠来减慢程序的执行速度。如果对这一行加注解, 会发现 The Count of Monte Cristo 的加 载相当快, 只有几批用户接口更新。
//程序清单 14-14 swingWorker/SwingWorkerTest.java
package swingWorker;
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
{
private JFileChooser chooser;
private JTextArea textArea;
private JLabel statusLine;
private JMenuItem openItem;
private JMenuItem cancelItem;
private SwingWorker<StringBuilder, ProgressData> textReader;
public static final int TEXT_ROWS = 20;
public static final int TEXT_COLUMNS = 60;
public SwingWorkerFrame()
{
chooser = new JFileChooser();
chooser.setCurrentDirectory(new File("."));
textArea = new JTextArea(TEXT_ROWS, TEXT_COLUMNS);
add(new JScrollPane(textArea));
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);
}
});
pack();
}
private class ProgressData
{
public int number;
public String line;
}
private class TextReader extends SwingWorker<StringBuilder, ProgressData>
{
private File file;
private StringBuilder text = new StringBuilder();
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;
try (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);
}
};
}
单一线程规则
每一个 Java 应用程序都开始于主线程中的 main方法。在 Swing 程序中,main方法的 生命周期是很短的。它在事件分配线程中规划用户界面的构造然后退出。在用户界面构造之 后,事件分配线程会处理事件通知, 例如调用 actionPerformed 或 paintComponent。其他线程 在后台运行, 例如将事件放入事件队列的进程,但是那些线程对应用程序员是不可见的。
对于单一线程规则存在一些例外情况。
•可在任一个线程里添加或移除事件监听器。 当然该监听器的方法会在事件分配线程中 被触发。 •只有很少的 Swing方法是线程安全的。在 API 文档中用这样的句子特别标明: “ 尽管 大多数 Swing 方法不是线程安全的, 但这个方法是。 ” 在这些线程安全的方法中最有 用的是:
JTextComponent.setText
JTextArea.insert
JTextArea.append
JTextArea.replaceRange
JCouponent.repaint
JComponent.revalidate
浙公网安备 33010602011771号