关键字:Observer Pattern、Java Thread、Java Swing Application
1 近来的阅读
近来寒暑不常,希自珍慰。武汉天气不是狂冷,就是狂热,不时还给我整个雪花,就差冰雹了。
自己做的事吧,也没有什么劲儿。看看自己喜欢的东西,等着希望中的学校能给我offers(是复数),看着自己想去又不想去的公司的未来同事在群里面幻想未来的样子,别操你大爷了,都成人这么久了,咋就还不了解这个世道呢?你给老板赚十块钱,老板自然会给你一块。如果老板的周围会赚钱的人不多,你还可以尝试吆喝更高的价钱。就这么个事,幻想有个鸟用。还不如静下心来好好学点儿东西,想想将来怎么多多给老板赚钱。无论以后打工还是自己当老板,路都得这么走出来。
另外,就是思考的毕设的事情。毕竟,让我立刻狠心的fuck掉这个学位也是不现实的。所以,继续搞,搞一个题目叫做“基于复杂网络的可度量防垃圾邮件系统”的论文加实验系统。看名字很BT,可我觉得这已经是我觉得最有意思的毕设题目了。我一定要做出点东西来。
无聊看书加上要完成毕设的实验系统,于是有了些动力,就产生了本篇小随笔。权当备忘吧。
看着“观察者模式”,琢磨琢磨,感觉跟别的模式有那么一点不一样。但是又说不出来,感觉使用这个模式的环境,为多线程为多。然后,就转而去看线程的书,看着线程的书,又发现毕设要用的Swing是学习理解线程很好的素材,而且Swing中的event-dispatch机制就是“观察者模式”,于是又转而去看讲Swing的书及专栏文章。
到此,这3个东西是彻底扯在一起了。总结一下它们之间的关系:Swing是建立在event-dispatch机制及Swing Single Thread下的多线程GUI编程环境。当然,在很多情况下,你写Swing也不大会用到(察觉)多线程(如我们的java课本中的Swing例子),但是,使用它的可能性已经大大增加了。而且要写好Swing,不理解线程是很困难的。
2 Java Thread
2.1 操作系统及面向过程
对于我这个新手来讲,使用多线程的机会不多。天天搞J2EE的应用开发,也根本用不上这个东西。但是,不理解你,不使用你,何谈功力的提升呢。你不找我,我也要找你。
在面向对象的世界中,这个Thread让我真的不好理解。它仿佛和我们自己写的对象,JDK的别的API类库都不一样,咋一看,看不出它的“使用价值”。(我理解的对象都有很“明显”的“使用价值”,呵呵)。
因为更多地谈论“进程”、“线程”还是在操作系统层面,记得在OS课本中有好几个章节都是讲“进程”相关的知识:调度算法、调度队列、调度优先级等等。后来上Unix和Linux的时候重点更是folk和进程间通讯(IPC)机制。但都是停留在理论理解,应付考试的阶段。而且编写的代码也都是面向过程的风格,没有“对象”的感觉。
Java能把OS层的概念加进来并予以实现,功能上很强,另外也给新手接触底层编程的机会。但是,把OS层的线程概念及它的面向过程编写风格与面向对象普遍的风格一比较,对Java Thread对象的理解就有点迷糊了。
2.2 从接口的本质思考
一般写程序不会太关注main(),特别是J2EE的东西搞多了,更是把它给忘尽了。main是程序的开始,是JVM的主线程。线程是另一个main(这样理解肯定不准确,但是有极大雷同),是欲并行执行的程序代码序列。
在java中,如果在main之外,还要执行独立并行的程序代码序列,就要用到java线程了。方法也比较简单:把独立执行的代码序列写到一个Runnable对象的run方法中,然后在父进程(很有可能就是main线程)中start它(先要把Runnable变成Thread对象)就可以了。
接口描述的是一种功能,而不是事物本质(类来描述)。如果我们把“能独立并行执行”当作一个对象的能力的话,把线程概念融入面向对象就容易多了。起码我是这么理解的。
至于你是喜欢给已有的类添加一个这个功能(也就是让已有的类来实现Runnable接口),还是另外专门写一个类,专门封装这种功能。就见仁见智了。正如我都喜欢把main独立写在一个控制类中,而不是放在功能类中一样。
2.3 匿名的嵌套类
也正是因为线程的并行性本质,在我们的代码中,那里要并行执行,我们就在这里开一个新的线程。而这时,为了最简便,对代码结构影响最小,最常见方式的就是开一个匿名的嵌套线程。
例子:如果doNoSequence()需要并行于执行doJob()的线程而独立执行。
1Void doJob(){
2
3 doFirst();
6
7 //need to be excuted in concurrently.
9 doNoSequence();
11
13 doSecond();
14
15}
16
就可以为它开一个匿名的嵌套线程。
1Void doJob(){
2 doFirst();
3
4 new Thread(){
5 public void run() {
6 doNoSequence();
7 }
8 }.start();
9
10 doSecond();
11}
12
2.4 异步的本质,同步的需要
“独立并行”的感觉一般开始都很好,至于以后,呵呵,不好说。
特别是当系统已经设计好了以后,在代码中按上面的例子随便开新线程。看似操作容易,但如果开的线程一多,相互之间的联系一多,以后就很难理解、管理和维护(许多article作者的话,我没有经验)。
doNoSequence()现在就开始难受了,因为它发现它和主线程还有很多联系,如和doFirst()有共同access的对象;还如doSecond()接受它返回的参数,一定要等它返回才能继续执行(也就是doNoSequence()对于主线程是block的)。明显,这里都需要“同步”。
当然,也不必绝望,只不过在代码里面继续添加更多的我不熟练的线程控制代码。线程开的越多,线程有关的关键字和控制代码就会出现的更多。最终的代码也许会面目全非,越来越难看出核心逻辑了。线程代码多了是小,但是核心的业务逻辑被淹没了是不允许的。
最好的解决方法,当然就是在设计的时候,多考虑一下程序并行化的可能。把有可能并行的逻辑分开来写。换句话讲,也就是要增加并行程序内部的耦合性,降低并行程序之间的耦合性。以便将来并不并行都容易使用。
如要编写既能异步执行,又能相对容易实现同步控制的代码,不少人(当然,都是大牛人)推荐使用“观察者模式”来重构你的系统。
3 观察者模式
“观察者模式(Observer Pattern)”是一个好东西,我以前实在是太孤陋寡闻了,竟然没有好好的去了解这样一个被广泛使用的模式,它给我带来很多的思考。通过它,使我更容易理解很多框架,资料,文章,代码。因为,当我一看到listener,event,observer等词语,就知道其设计原型为“观察者模式”。
“观察者模式”通过定义抽象主题[Abstrsct Subject](也叫消息源[Message Source]、事件源[Evnet Source]、被观察者),抽象观察者[Abstract Observer](更多的叫监听者[listener]),及它们之间的所谓事件[Event](也叫消息[Message])来实现的。这里的事件其实是对被观察者和观察者之间传递信息的一个封装,如果你觉得没有必要封装什么,那你用个String或int传递信息也是可以的。
自己再写两个类实现(Implement)上面两个抽象类就完成了一个“观察者模式”。
下面尝试举例说明,想必大家都经常被各种垃圾短信所困扰,一般这些短信都是推荐你去订阅一些服务,如每日一警句,足球彩票预测,色情资讯等等。如果你按照上面的number回了信息,就等于订阅了,也就要付钱了。为了防止你过分反感这个例子,影响叙述,那我就找个稍微有用那么一点的短信订阅服务来举例――“天气预报”。
提供服务的“天气预报提供商”就是模式中的“被观察者”,而订阅了服务的“订阅者”就是模式中的“观察者”。“天气预报服务商啊,我已经订阅你的天气服务啦,我盯上你啦,你一旦有了明天的天气情况就要及时通知我哦!”――这样粗俗的描述或许会让你更加容易理解,为什么天气预报服务商被称为“被观察者”,而我们被称为“观察者”,而我们之间传递的信息就是天气情况。
例子的类图:
模拟订阅及发送天气预报的代码:
1package gs.blogExample.weatherService;
2
3public class SimulatedEnvironment {
4 public static void main(String[] args) {
5
6 //定义天气服务提供商(模式角色:被观察者)
7 WeatherServiceProvider myProvider = new WeatherServiceProvider();
8
9 //定义两个天气服务订阅者(模式角色:观察者)
10 Housewife hw = new Housewife();
11 BusDriver bd = new BusDriver();
12
13 //他们订阅天气服务
14 myProvider.addServiceListener(hw);
15 myProvider.addServiceListener(bd);
16
17 System.out.println("<<Date:2005-3-12>>");
18 //天气服务提供商得到了新的天气信息,武汉今天9度
19 //只要天气服务商得到了任何一个城市的新天气,就会通知订阅者
20 myProvider.setNewWeather(new Weather("WUHAN",9,10));
21
22 //housewife退订天气服务
23 myProvider.removeServiceListener(hw);
24
25 System.out.println("<<Date:2005-3-13>>");
26 //第二天,天气供应商又得到了新的信息,武汉今天35度
27 //武汉一天之间从9度变35度,是可信的
28 myProvider.setNewWeather(new Weather("WUHAN",35,36));
29 }
30}
31
模拟输出的结果如下:
<<Date:2005-3-12>>
Housewife receiving weatherInfo begins..........
Housewife said: “so cool,let's make huoguo!!”
Housewife receiving weatherInfo ended..........
BusDriver receiving weatherInfo begins..........
BusDriver said: “fine day,nothing to do.”
BusDriver receiving weatherInfo ended..........
<<Date:2005-3-13>>
BusDriver receiving weatherInfo begins..........
BusDriver said: “so hot,open air condition!!”
BusDriver receiving weatherInfo ended..........
说明:订阅者接到天气预报后的行为就是“随便打印一句感受”。也就是说,例子中订阅者Print出一句话就代表他收到天气预报了。
从上面的输出结果可以看到,当Housewife在2005-3-12号退订了天气服务。的确,第二天天气预报提供商就没有再给它提供服务了。
继续拓展这个系统,使之提供另一种服务――“足球贴士服务”,根据“观察者模式”的角色,添加“足球贴士提供商”(被观察者)、“足球贴士订阅者”(观察者)和事件类“足球贴士事件”,另还有一个辅助描述具体足球贴士信息的类“足球贴士信息”。
继续类图:
模拟发送接受天气预报和足球贴士的代码:
1package gs.blogExample.weatherService;
2
3public class SimulatedEnvironment {
4 public static void main(String[] args) {
5
6 //定义天气服务提供商(模式角色:被观察者)
7 WeatherServiceProvider weatherServiceProvider = new WeatherServiceProvider();
8
9 //定义天气服务提供商(模式角色:被观察者)
10 SoccerTipServiceProvider soccerTipServiceProvider = new SoccerTipServiceProvider();
11
12 //定义两个天气服务订阅者(模式角色:观察者)
13 Housewife hw = new Housewife();
14 BusDriver bd = new BusDriver();
15
16 //hw、bd都订阅天气服务,bd还订阅了足球贴士服务
17 weatherServiceProvider.addServiceListener(hw);
18 weatherServiceProvider.addServiceListener(bd);
19 soccerTipServiceProvider.addServiceListener(bd);
20
21 System.out.println("<<Date:2005-3-12>>");
22 //天气服务提供商得到了新的天气信息,武汉今天9度
23 //足球贴士提供商得到了新的预测信息,MAN VS ASL 预测是1
24 weatherServiceProvider.setNewWeather(new Weather("WUHAN",9,10));
25 soccerTipServiceProvider.setNewSoccerTip(new SoccerTip("MAN VS ASL","2005-4-5","1"));
26
27 //housewife退订天气服务
28 weatherServiceProvider.removeServiceListener(hw);
29
30 System.out.println("<<Date:2005-3-13>>");
31 //第二天,天气供应商又得到了新的信息,武汉今天35度
32 weatherServiceProvider.setNewWeather(new Weather("WUHAN",35,36));
33 }
34}
35
模拟输出的结果如下:
<<Date:2005-3-12>>
Housewife receiving weatherInfo begins..........
Housewife said: "so cool,let's make huoguo!!"
Housewife receiving weatherInfo ended..........
BusDriver receiving weatherInfo begins..........
BusDriver said: "fine day,nothing to do."
BusDriver receiving weatherInfo ended..........
BusDriver receiving soccerTip begins..........
BusDriver said: "I am about to buy soccer lottery!!!I want to be rich!"
BusDriver receiving soccerTip ended..........
<<Date:2005-3-13>>
BusDriver receiving weatherInfo begins..........
BusDriver said: "so hot,open air condition!!"
BusDriver receiving weatherInfo ended..........
源码下载(JBuilder Project)
3.1 天生的异步模型
1 private void sendWeatherService() {
2
3 WeatherEvent we = new WeatherEvent(this.cityWeather);
4
5 Vector cloneListenerList = (Vector) getListenerList().clone();
6 Iterator iter = cloneListenerList.iterator();
7 while (iter.hasNext()) {
8 WeatherServiceListener listener =
9 (WeatherServiceListener) iter.next();
10 listener.WeatherArrived(we);
11 }
12
13}
14
继续上面的例子,天气预报提供商为订阅者发送天气预报的行为被封装在sendWeatherService方法中,而天气预报订阅者接受到天气预报信息后的行为被封装在WeatherArrived方法中。
从上面天气预报提供商的sendWeatherService方法的代码可以看到,提供商会遍历所有WeatherListener,然后通过调用它们的WeatherArrived方法来向它们传递WeatherEvent事件。
在这里,两个方法是在一个线程中运行的,也就是说,对于天气提供商来讲,如果第一个listener的WeatherArrived方法没有返回,那它就没有办法通知第二个listener。
具体到我们这个例子,我们是先通知Housewife再通知BusDriver的(由于遍历顺序的原因),也就是说是先执行Housewife的WeatherArrived方法,再执行BusDriver的WeatherArrived方法。试想,如果Housewife的WeatherArrived方法中要做的事情很多(往往家庭主妇就是如此),长期得不到返回,那么BusDriver的WeatherArrived方法就长期得不到执行,也就是说BusDriver长期得不到天气消息。对于天气这种时效性很强的信息,很明显,这样不行。是Bug!
自然的,可以运用Java Thraed,考虑把listener们的WeatherArrived并行化,放在不同的线程里去执行。这样,每当通知第一个listener都不会阻塞通知下一个listener。
的确,无论用不用观察者模式,你加几行java线程代码都可以实现并行化。只是,“观察者模式”无论在构思理解层面上,还是代码结构上,都特别容易向并行化过渡。
为什么这面说?一句话讲:“观察者模式”中的“被观察者角色”和“观察者角色”本生内在就有强烈的并行性特征。拥有“观察者模式”的系统一般都有这个特性,而有这个特性的系统也一般也可以设计成“观察者模式”。
3.2 设计并行系统的好方法
用Messge机制或Event机制(以“观察者模式”原型)来设计系统,可以更好的适应一旦程序有多线程的需要。因为在“观察者模式”中,我们在定义角色及事件时,其实它们已有并行的特征(也就是多线程的可能)。
这样可以让我们设计系统的时候,暂时不把主要的脑神经拿去考虑线程的复杂问题,放心的先把系统涉及的对象设计出来。而后来一旦有了线程化的需要,可以相对方便、清晰的添加,也不会出现太“拗眼”的代码群。
结论还是,有利于系统的清晰,维护的方便。
在Swing应用程序的设计中,就有人这样设计,并且把它上升为一种通用方法,可参见下面的Event-Dispatch方法章节。
4 Java Swing
Swing是Java的GUI类库。因此,Java的桌面应用程序,也被称为Swing应用程序。它的优点是Java的优点:“Write once,Run anywhere”;它的缺点也是Java的缺点:“性能问题要命”。
4.1 Swing单线程模型
桌面应用程序生命周期可以概括如下:
STEP 1:初始化界面;
STEP 2:等待你的event,主要还是鼠标和键盘event;
STEP 3:event一到,执行相应的业务逻辑代码;
STEP 4:根据结果,刷新界面,用户看到;
STEP 5:然后,又到步骤2,直到你退出。
所谓Swing单线程模型的意思就是,所有和“界面”有关系的代码都在Swing规定的线程(称为Swing线程)里按照顺序被执行,不可以用自己定义的线程跟“界面”有任何的接触(例外就不赘述了)。这里的“界面”指的是Jcomponent的子类,容器、按钮、画布都属于界面的范围。这里的“和界面有关系”包括event响应、repaint、改变属性等等。
简单来说,就是不能用自己的线程控制这些Jcomponent对象啦。
真衰,这么干的原因还不是让Java的桌面程序不至于太慢,结果所有(也有例外)Swing的类都被设计成“非”threadsafe的,因此也只能让一个线程(Swing线程)访问了。
4.2 Long-Running Event Callbacks
Swing编程中最大的一个麻烦就是处理“长时间事件响应”。对照上面生命周期的step3(event一到,执行相应的业务逻辑代码),“长时间事件响应”的意思就是Step3执行的时间太长了,方法长期返回不了,以至把整个Swing线程的生命周期阻塞了,用户看起来就像死机了。
而一般这种情况,这个“长时间事件响应”都是在Swing线程中执行的,因此阻塞了才会这么大件事。
那就把它移出Swing线程吧,让它自己到另外的线程里面去做。这样即使你阻塞了,也不至于阻塞Swing线程这个事关给用户感觉的线程。
但问题还没有完,尽管上面的step3,也就是这个“长时间事件响应”的确对Swing线程没有影响了,Swing线程不会被阻塞了。但是在新线程(称为N进程)里,程序到了step4(根据结果,刷新界面,用户看到), 它要根据step3的结果去刷新界面,把结果显示给用户。而根据Swing的单线程模型,N进程是没有权利去接触界面对象的,只有Swing线程才有这个权利。所以,身处N进程的step4又必须被放回到Swing线程里去执行。
这样,来来回回,麻麻烦烦,把我Swing晕了,怪不得叫Swing,真是“忽悠”的意思啊。这里得到灵感,以后都用忽悠来翻译Swing,呵呵。
5 Swing Thread Programming
为了把一些跟界面相关的代码放回到Swing线程里去执行,Swing类库中提供了方法,我懒得说了。
//把任务仍回给Swing执行,不阻塞本线程
invokeLater(Runnable GUITask)
//把任务仍回给Swing执行,阻塞本线程
invokeAndWait(Runnable GUITask)
当然,除了直接用这两个方法处理长时间事件响应外,这几天在网上还看到了另外两个方法。
5.1 SwingWorker
提供了一个帮助类SwingWorker,使用方便。它把这些来来回回,麻麻烦烦的线程逻辑封装起来。简化使用Swing多线程的复杂度。
详细了解,请点击相关页面。不好访问,也可以本站下载pdf版本。
5.2 Event-Dispatch方法
使用观察者模式重构系统,优化Swing多线程程序的设计。方法基本近似上文的“设计并行程序的好办法”。它和SwingWork最大的区别就是,使用它是要重新设计系统的,会产生基于“观察者模式”的角色类的。
详细了解,请点击相关页面。不好访问,也可以本站下载pdf版本。
6 结论
想借Swing Thread Programming探讨一下多线程设计的一些初级问题,并推荐了一下“观察者模式”。