Java学习day100 并发(十)(线程与Swing:运行耗时的任务、使用Swing工作线程、单一线程规则)

使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。

day100   并发(十)(线程与Swing:运行耗时的任务、使用Swing工作线程、单一线程规则)

在程序中使用线程的理由之一是提高程序的响应性能。当程序需要做某些耗时的工作时,应该启动另一个工作器线程而不是阻塞用户接口。但是,必须认真考虑工作器线程在做什么,因为这或许令人惊讶,Swing不是线程安全的。如果你试图在多个线程中操纵用户界面的元素,那么用户界面可能崩溃。

要了解这一问题,运行下面的测试程序。当你点击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按钮,不断点击组合框。最终,你会看到一个异常报告。

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

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


1.运行耗时的任务

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

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

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

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

要解决这一问题,在任何线程中,可以使用两种有效的方法向事件队列添加任意的动作。例如,假定想在一个线程中周期性地更新标签来表明进度。

不可以从自己的线程中调用label.setText,而应该使用EventQueue类的invokeLater方法和invokeAndWait方法使所调用的方法在事件分配线程中执行。

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

EventQueue.invokeLater( ( )->{
label.setText(percentage+ "% complete");
});

当事件放人事件队列时,invokeLater方法立即返回,而run方法被异步执行。invokeAndWait方法等待直到run方法确实被执行过为止。

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

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

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

/**
 *@author  zzehao
 */
import java.awt.*;
import java.util.*;
import javax.swing.*;

//This program demonstrates that a thread that runs in parallel with the event
public class SwingThreadiest
{
	public static void main(String[] args)
	{
		EventQueue.invokeLater(() -> {
			JFrame frame = new SwingThreadFrame();
			frame.setTitle("SwingThreadiest");
			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(event ->
			new Thread(new GoodWorkerRunnable(combo)).start());
		panel.add(goodButton);
		JButton badButton = new JButton("Bad");
		badButton.addActionListener(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(() -> 
				 {
					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)
		 {
		 }
	 }
 }

运行的结果是:


2.使用Swing工作线程

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

SwingWorker类使后台任务的实现不那么繁琐。

下面的程序有加载文本文件的命令和取消加载过程的命令。应该用一个长的文件来测试这个程序,该文件在一个单独的线程中加载。在读取文件的过程中,Open菜单项被禁用,Cancel菜单项为可用。读取每一行后,状态条中的线性计数器被更新。读取过程完成之后,Open菜单项重新变为可用,Cancel项被禁用,状态行文本置为Done。

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

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

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

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 number;
    public String line;
}

最后的结果是已经读人StringBuilder的文本。因此,需要一个SwingWorker<StringBuilder,ProgressData>。

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

@Override
		public StringBuilder doInBackground() throws IOException, InterruptedException
		{
			int lineNumber = 0;
			try (Scanner in = new Scanner(new FileInputStream(file), "UTF-8"))
			{
				while (in.hasNextLine())
				{
					String line = in.nextLine();
					lineNumber++;
					text.append(line).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毫秒,以便不使用重读就可以检测取消动作,但是,不要使用休眠来减慢程序的执行速度。如果对这一行加注解,会发现TheCountofMonteCristo的加载相当快,只有几批用户接口更新。

在这个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).append("\n");
			textArea.append(b.toString());
		}

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

在Open菜单项的事件监听器中,工作器是如何启动的。

这一简单的技术允许人们在保持对用户界面的正常响应的同时,执行耗时的任务。

/**
 *@author  zzehao
 */
import java.awt.*;
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.
public class  SwingWorkerTest
{
	public static void main(String[] args) throws Exception
	{
		EventQueue.invokeLater(() -> {
			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(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(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), "UTF-8"))
			{
				while (in.hasNextLine())
				{
					String line = in.nextLine();
					lineNumber++;
					text.append(line).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).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);
		}
	};
}

运行的结果:


3.单一线程规则

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

前面介绍了单一线程规则:“除了事件分配线程,不要在任何线程中接触Swing组件。”

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

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

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

]TextComponent.setText
ITextArea.insert
JTextArea.append
JTextArea.replaceRange
JCouponent.repaint
JComponent.revalidate

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

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

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

检测并定位这些问题可能会好些,但是Swing的设计者没有走这条轻松的路。

他们认定除了使用事件分配线程之外,从任何其他线程访问组件永远都是不安全的。因此,你需要在事件分配线程构建用户界面,像程序示例中那样调用EventQueue.invokeLater。

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


至此我们已经学完了这本Java入门级经典书籍,掌握了Java程序设计语言的基础知识以及大多数编程项目所需要的标准库中的部分内容。在学习Java基础知识的过程中感到愉快并得到了有用的信息。坚持写博客也是读完这本七百多页书的动力之一,继续加油!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值