|
不正的SwingU程是运行缓慢、无响应和不E_的Swing应用的主要原因之一。这是许多原因造成的,从开发h员对Swing单线E模型的误解Q到保证正确的线E执行的困难。即使对SwingU程q行了很多努力,应用U程逻辑也是很难理解和维护的。本文阐qC如何在开发Swing应用中用事仉动编E,以大大简化开发、维护,q提供高灉|性? 背景 既然我们是要化Swing应用的线E,首先让我们来看看SwingU程是怎么工作的,Z么它是必ȝ。Swing API是围l单U程模型设计的。这意味着Swinglg必须L通过同一个线E来修改和操Ucؓ什么采用单U程模型Q这有很多原因,包括开发成本和同步Swing的复杂性-Q这都会造成一个迟钝的API。ؓ了达到单U程模型Q有一个专门的U程用于和Swinglg交互。这个线E就是大家熟知的SwingU程QAWTQ有时也发音为“ought”)U程Q或者事件分zE。在本文的下面的部分Q我选用SwingU程的叫法? 既然SwingU程是和Swinglgq行交互的唯一的线E,它就被赋予了很多责Q。所有的l制和图形,鼠标事gQ组件事Ӟ按钮事gQ和所有其它事仉发生在SwingU程。因为SwingU程的工作已l非常沉重了Q当太多其它工作在SwingU程中进行处理时׃发生问题。会引vq个问题的最常见的位|是在非Swing处理的地方,像发生在一个事件监听器Ҏ中,比如JButton的ActionListenerQ的数据库查找。既然ActionListener的actionPerformed()Ҏ自动在SwingU程中执行,那么Q数据库查找也将在SwingU程中执行。这占用了Swing的工作,L它处理它的其它Q务-Q像l制Q响应鼠标移动,处理按钮事gQ和应用的羃放。用户以为应用死掉了Q但实际上ƈ不是q样。在适当的线E中执行代码对确保系l正常地执行非常重要? 既然我们已经看到了在适当的线E中执行Swing应用的代码是多么重要Q现在让我们如何实现q些U程。我们看看将代码攑օ和移出SwingU程的标准机制。在讲述q程中,我将H出几个和标准机制有关的问题和难炏V正如我们看到的Q大部分的问题都来自于企囑֜异步的SwingU程模型上实现同步的代码模型。从那儿Q我们将看到如何修改我们的例子到事g驱动Q-UL整个方式到异步模型?
private void searchButton_actionPerformed() { outputTA.setText("Searching for: " + searchTF.getText()); //Broken!! Too much work in the Swing thread String[] results = lookup(searchTF.getText()); outputTA.setText(""); for (int i = 0; i < results.length; i++) { String result = results[i]; outputTA.setText(outputTA.getText() + '\n' + result); } } |
private void searchButton_actionPerformed() { outputTA.setText("Searching for: " + searchTF.getText()); //the String[][] is used to allow access to // setting the results from an inner class final String[][] results = new String[1][1]; new Thread() { public void run() { results[0] = lookup(searchTF.getText()); } }.start(); outputTA.setText(""); for (int i = 0; i < results[0].length; i++) { String result = results[0][i]; outputTA.setText(outputTA.getText() + '\n' + result); } } |
private void searchButton_actionPerformed() { outputTA.setText("Searching for: " + searchTF.getText()); final String[][] results = new String[1][1]; new Thread() { public void run() { //get results. results[0] = lookup(searchTF.getText()) // send runnable to the Swing thread // the runnable is queued after the // results are returned SwingUtilities.invokeLater( new Runnable() { public void run() { // Now we're in the Swing thread outputTA.setText(""); for (int i = 0; i < results[0].length; i++) { String result = results[0][i]; outputTA.setText( outputTA.getText() + '\n' + result); } } } ); } }.start();} |
问题 我们在企囑ּ刉过异步模型q行同步执行Q-企图一个方形的螺栓攑ֈ一个圆形的IZ。只有我们尝试这样做Q我们就会不断地遭遇q些问题。从我的l验Q可以告诉你q些代码很难阅读Q很隄护,q且易于出错? q看h是一个常见的问题Q所以一定有标准的方式来解决Q对吗?出现了一些框架用于管理Swing的复杂性,所以让我们来快速预览一下它们可以做什么? 一个可以得到的解决Ҏ是FoxtrotQ一个由Biorn Steedom写的框架Q可以在SourceForge上获取。它使用一个叫做Worker的对象来控制非Swingd在非SwingU程中的执行Q阻塞直到非Swingd执行完毕。它化了SwingU程Q允怽~写同步代码Qƈ在SwingU程和非SwingU程直接切换。下面是来自它的站点的一个例子: public void actionPerformed(ActionEvent e) { button.setText("Sleeping..."); String text = null; try { text = (String)Worker.post(new Task() { public Object run() throws Exception { Thread.sleep(10000); return "Slept !"; } } ); } catch (Exception x) ... button.setText(text); somethingElse();} 注意它是如何解决上面的那些问题的。我们能够非常容易地在SwingU程中传入传出变量。ƈ且,代码块看h也很正确Q-先编写的先执行。但是仍然有一些问题障阻止用从准同步异步解x案。Foxtrot中的一个问题是异常理。用FoxtrotQ每ơ调用Worker必须捕获Exception。这是将执行代理lWorker来解军_步对异步问题的一个物? 同样以非常相似的方式Q我此前也创Z一个框Ӟ我称它ؓ链接q行引擎QChained Runnable EngineQ?Q同样也遭受来自cM同步对异步问题的困扰。用这个框Ӟ你将创徏一个将被引擎执行的Runnable的集合。每一个Runnable都有一个指C器告诉引擎是否应该在SwingU程或者另外的U程中执行。引擎也保证Runnable以正的序执行。所以Runnable #2不会放入队列直到Runnable #1执行完毕。ƈ且,它支持变量以HashMap的Ş式从Runnable到Runnable传递? 表面上,它看h解决了我们的主要问题。但是当你深入进dQ同L问题又冒出来了。本质上Q我们ƈ没有改变上面描述的Q何东西-Q我们只是将复杂性隐藏在引擎的后面。因为指数增长的Runnable而代码~写变得非常枯燥,也很复杂Qƈ且这些Runnable常常怺耦合。Runnable之间的非cd的HashMap变量传递变得难于管理。问题的列表q有很多? 在编写这个框架之后,我意识到q需要一个完全不同的解决Ҏ。这让我重新审视了问题,看别人是怎么解决cM的问题的Qƈ深入的研I了Swing的源代码?
解决ҎQ事仉动编E? 所有前面的q些解决Ҏ都存在一个共同的致命~陷Q-企图在持l地改变U程的同时表CZ个Q务的功能集。但是改变线E需要异步的模型Q而线E异步地处理Runnable。问题的部分原因是我们在企图在一个异步的U程模型之上实现一个同步的模型。这是所有Runnable之间的链和依赖,执行序和内部类scooping问题的根源。如果我们可以构建真正的异步Q我们就可以解决我们的问题ƈ极大地简化SwingU程? 在这之前Q让我们先列举一下我们要解决的问题: 1. 在适当的线E中执行代码 2. 使用SwingUtilities.invokeLater()异步地执? 异步地执行导致了下面的问题: 1. 互相耦合的组? 2. 变量传递的困难 3. 执行的顺? 让我们考虑一下像Java消息服务QJMSQ这LZ消息的系l,因ؓ它们提供了在异步环境中功能组件之间的松散耦合。消息系l触发异步事Ӟ正如在Enterprise Integration Patterns 中描q的。感兴趣的参与者监听该事gQƈ对事件做成响应-Q通常通过执行它们自己的一些代码。结果是一l模块化的,松散耦合的组Ӟlg可以d到或者从pȝ中去除而不影响到其它组件。更重要的,lg之间的依赖被最化了,而每一个组仉是良好定义的和封装的Q-每一个都仅对自己的工作负责。它们简单地触发消息Q其它一些组件将响应q个消息Qƈ对其它组件触发的消息q行响应? 现在Q我们先忽略U程问题Q将lg解耦ƈUL到异步环境中。在我们解决了异步问题后Q我们将回过头来看看U程问题。正如我们所要看到的,那时解决q个问题非常容易? 让我们还拿前面引入的例子Qƈ把它UL到基于事件的模型。首先,我们把lookup调用抽象C个叫LookupManager的类中。这允许我们将所有UIcM的数据库逻辑UdQƈ最l允许我们完全将q两者脱耦。下面是LookupManagercȝ代码Q? class LookupManager { private String[] lookup(String text) { String[] results = ... // database lookup code return results } } 现在我们开始向异步模型转换。ؓ了ɘq个调用异步化,我们需要抽象调用的q回。换句话Q方法不能返回Q何倹{我们将以分辨什么相关的动作是其它类所希望知道的开始。在我们q个例子中最明显的事件是搜烦l束事g。所以让我们创徏一个监听器接口来响应这些事件。该接口含有单个ҎlookupCompleted()。下面是接口的定义: interface LookupListener { public void lookupCompleted(Iterator results);} 遵守Java的标准,我们创徏另外一个称作LookupEvent的类包含l果字串数组Q而不是到处直接传递字串数l。这允许我们在不改变LookupListener接口的情况下传递其它信息。例如,我们可以在LookupEvent中同时包括查扄字串和结果。下面是LookupEventc: public class LookupEvent { String searchText; String[] results; public LookupEvent(String searchText) { this.searchText = searchText; } public LookupEvent(String searchText, String[] results) { this.searchText = searchText; this.results = results; } public String getSearchText() { return searchText; } public String[] getResults() { return results; } } 注意LookupEventcL不可变的。这是很重要的,因ؓ我们q不知道在传递过E中谁将处理q些事g。除非我们创Z件的保护拯来传递给每一个监听者,我们需要把事g做成不可变的。如果不q样Q一个监听者可能会无意或者恶意地修订事g对象Qƈ破坏pȝ? 现在我们需要在LookupManager上调用lookupComplete()事g。我们首先要在LookupManager上添加一个LookupListener的集合: List listeners = new ArrayList(); q提供在LookupManager上添加和去除LookupListener的方法: public void addLookupListener(LookupListener listener){ listeners.add(listener); } public void removeLookupListener(LookupListener listener){ listeners.remove(listener); } 当动作发生时Q我们需要调用监听者的代码。在我们的例子中Q我们将在查找返回时触发一个lookupCompleted()事g。这意味着在监听者集合上q代Qƈ使用一个LookupEvent事g对象调用它们的lookupCompleted()Ҏ?
我喜Ƣ把q些代码析取C个独立的Ҏfire[event-method-name] Q其中构造一个事件对象,在监听器集合上P代,q调用每一个监听器上的适当的方法。这有助于隔M要逻辑代码和调用监听器的代码。下面是我们的fireLookupCompletedҎQ? private void fireLookupCompleted(String searchText, String[] results){ LookupEvent event = new LookupEvent(searchText, results); Iterator iter = new ArrayList(listeners).iterator(); while (iter.hasNext()) { LookupListener listener = (LookupListener) iter.next(); listener.lookupCompleted(event); } } W?行代码创Z一个新的集合,传入原监听器集合。这在监听器响应事g后决定在LookupManager中去除自己时发挥作用。如果我们不是安全地拯集合Q在一些监听器应该 被调用而没有被调用时发生o人厌烦的错误? 下面Q我们将在动作完成时调用fireLookupCompleted辅助Ҏ。这是lookupҎ的返回查询结果的l束处。所以我们可以改变lookupҎ使其触发一个事件而不是返回字串数l本w。下面是新的lookupҎQ? public void lookup(String text) { //mimic the server call delay... try { Thread.sleep(5000); } catch (Exception e){ e.printStackTrace(); } //imagine we got this from a server String[] results = new String[]{"Book one", "Book two", "Book three"}; fireLookupCompleted(text, results); } 现在让我们把监听器添加到LookupManager。我们希望当查找q回时更新文本区域。以前,我们只是直接调用setText()Ҏ。因为文本区域是和数据库调用一起都在UI中执行的。既然我们已l将查找逻辑从UI中抽象出来了Q我们将把UIcMZ个到LookupManager的监听器Q监听lookup事gq相应地更新自己。首先我们将在类定义中实现监听器接口Q? public class FixedFrame implements LookupListener 接着我们实现接口ҎQ? public void lookupCompleted(final LookupEvent e) { outputTA.setText(""); String[] results = e.getResults(); for (int i = 0; i < results.length; i++) { String result = results[i]; outputTA.setText(outputTA.getText() + "\n" + result); } } 最后,我们它注册为LookupManager的一个监听器Q? public FixedFrame() { lookupManager = new LookupManager(); //here we register the listener lookupManager.addListener(this); initComponents(); layoutComponents();} Z化,我在cȝ构造器中将它添加ؓ监听器。这在大多数pȝ上都允许良好。当pȝ变得更加复杂Ӟ你可能会重构、从构造器中提炼出监听器注册代码,以允许更大的灉|性和扩展性? 到现在ؓ止,你看C所有组件之间的q接Q注意职责的分离。用L面类负责信息的显C-Qƈ且仅负责信息的显C。另一斚wQLookupManagerc负责所有的lookupq接和逻辑。ƈ且,LookupManager负责在它变化旉知监听器-Q而不是当变化发生时应该具体做什么。这允许你连接Q意多的监听器? Z演示如何d新的事gQ让我们回头d一个lookup开始的事g。我们可以添加一个称作lookupStarted()的事件到LookupListenerQ我们将在查扑ּ始执行前触发它。我们也创徏一个fireLookupStarted()事g调用所有LookupListener的lookupStarted()。现在lookupҎ如下Q? public void lookup(String text) { fireLookupStarted(text); //mimic the server call delay... try { Thread.sleep(5000); } catch (Exception e){ e.printStackTrace(); } //imagine we got this from a server String[] results = new String[]{"Book one", "Book two", "Book three"}; fireLookupCompleted(text, results);} 我们也添加新的触发方法fireLookupStarted()。这个方法等同于fireLookupCompleted()ҎQ除了我们调用监听器上的lookupStarted()ҎQƈ且该事g也不包含l果集。下面是代码Q? private void fireLookupStarted(String searchText){ LookupEvent event = new LookupEvent(searchText); Iterator iter = new ArrayList(listeners).iterator(); while (iter.hasNext()) { LookupListener listener = (LookupListener) iter.next(); listener.lookupStarted(event); } } 最后,我们在UIcM实现lookupStarted()ҎQ设|文本区域提C当前搜索的字符丌Ӏ? public void lookupStarted(final LookupEvent e) { outputTA.setText("Searching for: " + e.getSearchText()); } q个例子展示了添加新的事件是多么Ҏ。现在,让我们看看展CZ仉动脱耦的灉|性。我们将通过创徏一个日志类Q当一个搜索开始和l束时在命o行中输出信息来演C。我们称q个cMؓLogger。下面是它的代码Q? public class Logger implements LookupListener { public void lookupStarted(LookupEvent e) { System.out.println("Lookup started: " + e.getSearchText()); } public void lookupCompleted(LookupEvent e) { System.out.println("Lookup completed: " + e.getSearchText() + " " + e.getResults()); } } 现在Q我们添加Logger作ؓ在FixedFrame构造方法中的LookupManager的一个监听器? public FixedFrame() { lookupManager = new LookupManager(); lookupManager.addListener(this); lookupManager.addListener(new Logger()); initComponents(); layoutComponents(); } 现在你已l看Cd新的事g、创建新的监听器Q-向您展示了事仉动方案的灉|性和扩展性。你会发现随着你更多地开发事仉中的E序Q你会更加娴熟地在你的应用中创徏通用动作。像其它所有事情一Pq只需要时间和l验。看h在事件模型上已经做了很多研究Q但是你q是需要把它和其它替代Ҏ相比较。考虑开发时间成本;最重要的,q是一ơ性成本。一旦你创徏好了监听器模型和它们的动作,以后向你的应用中d监听器将是小菜一蝶?
U程 到现在,我们已经解决了上面的异步问题Q通过监听器ɾlgp,通过事g对象传递变量,通过事g产生和监听器的注册的l合军_执行的顺序。让我们回到U程问题Q因为正是它把我们带Cq儿。实际上非常ҎQ因为我们已l有了异步功能的监听器,我们可以单地让监听器自己军_它们应该在哪个线E中执行。考虑UIcdLookupManager的分RUIcd于事Ӟ军_需要什么处理。ƈ且,该类也是SwingQ而日志类不是。所以让UIc负责决定它应该在什么线E中执行更加有意义。所以,让我们再ơ看看UIcR下面是没有U程的lookupCompleted()ҎQ? public void lookupCompleted(final LookupEvent e) { outputTA.setText(""); String[] results = e.getResults(); for (int i = 0; i < results.length; i++) { String result = results[i]; outputTA.setText(outputTA.getText() + "\n" + result); } } 我们知道q将在非SwingU程中调用,因ؓ该事件是直接在LookupManager中触发的Q这不是在SwingU程中执行。因为所有的代码功能上都是异步的Q我们不必等待监听器Ҏ允许l束后才调用其它代码Q,我们可以通过SwingUtilities.invokeLater()这些代码改道到SwingU程。下面是新的ҎQ传入一个匿名Runnable到SwingUtilities.invokeLater(): public void lookupCompleted(final LookupEvent e) { //notice the threading SwingUtilities.invokeLater( new Runnable() { public void run() { outputTA.setText(""); String[] results = e.getResults(); for (int i = 0; i < results.length; i++) { String result = results[i]; outputTA.setText(outputTA.getText() + "\n" + result); } } } ); } 如果MLookupListener不是在SwingU程中执行,我们可以在调用线E中执行监听器代码。作Z个原则,我们希望所有的监听器都q速地接到通知。所以,如果你有一个监听器需要很多时间来处理自己的功能,你应该创Z个新的线E或者把耗时代码攑օThreadPool中等待执行? 最后的步骤是让LookupManager在非SwingU程中执行lookup。当前,LookupManager是在JButton的ActionListener的SwingU程中被调用的。现在是我们做出军_的时候,或者我们在JButton的ActionListener中引入一个新的线E,或者我们可以保证lookup自己在非SwingU程中执行,自己开始一个新的线E。我选择可能和Swingc脓q地理SwingU程。这有助于把所有Swing逻辑装在一赗如果我们把SwingU程逻辑d到LookupManagerQ我们将引入了一层不必要的依赖。ƈ且,对于LookupManager在非SwingU程环境中孵化自qU程是完全没有必要的Q比如一个非l图的用L面,在我们的例子中,是Logger。生不必要的新U程损宛_你应用的性能Q而不是提高性能。LookupManager执行的很好,不管SwingU程与否Q-所以,我喜Ƣ把代码集中在那ѝ? 现在我们需要将JButton的ActionListener执行lookup的代码放在一个非SwingU程中。我们创Z个匿名的ThreadQ用一个匿名的Runnable执行q个lookup? private void searchButton_actionPerformed() { new Thread(){ public void run() { lookupManager.lookup(searchTF.getText()); } }.start(); } q就完成了我们的SwingU程。简单地在actionPerformed()Ҏ中添加线E,保监听器在新的U程中执行照ֈ了整个线E问题。注意,我们不用处理像第一个例子那LM问题。通过把时间花费在定义一个事仉动的体系Q我们在和SwingU程相关处理上节U了更多的时间? l论 如果你需要在同一个方法中执行大量的Swing代码和非Swing代码Q很Ҏ某些代码放错位|。事仉动的方式迫使你代码放在它应该在的地方Q-它仅应该在的地方。如果你在同一个方法中执行数据库调用和更新UIlgQ那么你在一个类中写入了太多的逻辑。分析你pȝ中的事gQ创建底层的事g模型迫使你代码放到正的地方。将Ҏ的数据库调用代码攑֜非UIcMQ也不要在非UIlg中更新UIlg。采用事仉动的体系QUI负责UI更新Q数据库理c负责数据库调用。在q一点上Q每一个封装的c都只用兛_自己的线E,不用担心pȝ其它部分如何动作。当Ӟ设计、构Z个事仉动的客户端也很有用,但是需要花费的旉代hq超q带来的l果pȝ的灵zL和可维护性的提高?
public class MyThread_1 extends Thread
{
public void run()
{
//some code
}
}
public class MyThread_2 implements Runnable
{
public void run()
{
//some code
}
}
new MyThread_1().start()
new Thread(new MyThread_2()).start()
public class MyThread implements Runnable
{
public void run()
{
System.out.println("My Name is "+Thread.currentThread().getName());
}
public static void main(String[] args)
{
new Thread(new MyThread()).start();
}
}
new Thread(new MyThread()).start();
new Thread(new MyThread()).start();
new Thread(new MyThread()).start();
public class MyThread implements Runnable
{
public void run()
{
System.out.println("My Name is "+Thread.currentThread().getName());
}
public static void main(String[] args)
{
Thread t1=new Thread(new MyThread());
Thread t2=new Thread(new MyThread());
Thread t3=new Thread(new MyThread());
t2.setPriority(Thread.MAX_PRIORITY);//赋予最高优先
t1.start();
t2.start();
t3.start();
}
}
public class MyThread implements Runnable
{
public void run()
{
try
{
int sleepTime=(int)(Math.random()*100);//产生随机数字Q?br />Thread.currentThread().sleep(sleepTime);//让其休眠一定时_旉又上面sleepTime军_
//public static void sleep(long millis)throw InterruptedException QAPIQ?br />System.out.println(Thread.currentThread().getName()+" 睡了 "+sleepTime);
}catch(InterruptedException ie)//׃U程在休眠可能被中断Q所以调用sleepҎ的时候需要捕捉异?br />{
ie.printStackTrace();
}
}
public static void main(String[] args)
{
Thread t1=new Thread(new MyThread());
Thread t2=new Thread(new MyThread());
Thread t3=new Thread(new MyThread());
t1.start();
t2.start();
t3.start();
}
}
public static void main(String[] args)
{
/***************************************
ThreadGroup(String name)
ThreadGroup(ThreadGroup parent, String name)
***********************************/
ThreadGroup group1=new ThreadGroup("group1");
ThreadGroup group2=new ThreadGroup(group1,"group2");
Thread t1=new Thread(group2,new MyThread());
Thread t2=new Thread(group2,new MyThread());
Thread t3=new Thread(group2,new MyThread());
t1.start();
t2.start();
t3.start();
}
class BlankSaving //储蓄账户
{
private static int money=10000;
public void add(int i)
{
money=money+i;
System.out.println("Husband 向银行存入了 [K?+i+"]");
}
public void get(int i)
{
money=money-i;
System.out.println("Wife 向银行取C [K?+i+"]");
if(money<0)
System.out.println("余额不Q?);
}
public int showMoney()
{
return money;
}
}
class Operater implements Runnable
{
String name;
BlankSaving bs;
public Operater(BlankSaving b,String s)
{
name=s;
bs=b;
}
public static void oper(String name,BlankSaving bs)
{
if(name.equals("husband"))
{
try
{
for(int i=0;i<10;i++)
{
Thread.currentThread().sleep((int)(Math.random()*300));
bs.add(1000);
}
}catch(InterruptedException e){}
}else
{
try
{
for(int i=0;i<10;i++)
{
Thread.currentThread().sleep((int)(Math.random()*300));
bs.get(1000);
}
}catch(InterruptedException e){}
}
}
public void run()
{
oper(name,bs);
}
}
public class BankTest
{
public static void main(String[] args)throws InterruptedException
{
BlankSaving bs=new BlankSaving();
Operater o1=new Operater(bs,"husband");
Operater o2=new Operater(bs,"wife");
Thread t1=new Thread(o1);
Thread t2=new Thread(o2);
t1.start();
t2.start();
Thread.currentThread().sleep(500);
}
}
class MyThread_1 extends Thread
{
Object lock;
public MyThread_1(Object o)
{
lock=o;
}
public void run()
{
try
{
synchronized(lock)
{
System.out.println("Enter Thread_1 and wait");
lock.wait();
System.out.println("be notified");
}
}catch(InterruptedException e){}
}
}
class MyThread_2 extends Thread
{
Object lock;
public MyThread_2(Object o)
{
lock=o;
}
public void run()
{
synchronized(lock)
{
System.out.println("Enter Thread_2 and notify");
lock.notify();
}
}
}
public class MyThread
{
public static void main(String[] args)
{
int[] in=new int[0];//notice
MyThread_1 t1=new MyThread_1(in);
MyThread_2 t2=new MyThread_2(in);
t1.start();
t2.start();
}
}