看了Thinking in java上的多线程后颇有感触,著文记之。在这里我假设各位都已经拥有了awt和applet的初级知识。
所谓线程,即计算机进程内部的子执行模块,从具体表现上来说,就是独立分配到一部分系统资源而独立于其他可执行代码执行的可执行代码。在java中,实现了语言上的多线程编程。Thinking in java中举了一系列的几个例子来阐述其较为表层的特点。
对于一个拥有ui的程序来说,控制其ui的代码段一般运行在一个线程中,如果想要时刻刷新ui中某些控件的外观,则最好在另一个线程中进行操作,否则可能导致ui线程的阻塞,从而让用户感觉到应用程序停止了交互(例如点击按钮没有了反应等)。例如下面这个取自Thinking in java的计时器案例:
1 packagecom.william.myclient; 2 3 importjava.awt.*; 4 importjava.awt.event.*; 5 importjava.applet.*; 6 7 publicclassCounter1extendsApplet{ 8 9 privateintcount=0; 10 privateButtononOff=newButton("Toggle"),start=newButton("Start"); 11 privateTextFieldt=newTextField(10); 12 privatebooleanrunFlag=true; 13 14 publicvoidinit(){ 15 add(t); 16 start.addActionListener(newStartL()); 17 add(start); 18 onOff.addActionListener(newOnOffL()); 19 add(onOff); 20 } 21 22 publicvoidgo(){ 23 while(true){ 24 try{ 25 Thread.currentThread().sleep(100); 26 } 27 catch(InterruptedExceptione){ 28 29 } 30 if(runFlag) 31 t.setText(Integer.toString(count++)); 32 } 33 } 34 35 classStartLimplementsActionListener{ 36 publicvoidactionPerformed(ActionEvente){ 37 go(); 38 } 39 } 40 41 classOnOffLimplementsActionListener{ 42 publicvoidactionPerformed(ActionEvente){ 43 runFlag=!runFlag; 44 } 45 } 46 47 publicstaticvoidmain(String[]args){ 48 Counter1applet=newCounter1(); 49 FrameaFrame=newFrame("Counter1"); 50 aFrame.addWindowListener(newWindowAdapter(){ 51 publicvoidwindowClosing(WindowEvente){ 52 System.exit(0); 53 } 54 }); 55 aFrame.add(applet,BorderLayout.CENTER); 56 aFrame.setSize(300,200); 57 applet.init(); 58 applet.start(); 59 aFrame.setVisible(true); 60 } 61 }
按下start,运行这个程序后会发现,当按下暂停键时,程序不会做出任何的响应——尽管我们已经给它设置了让运行停止的监听器。而事实上是,除了
最大化最小化还有点反应外,基本上其他按键都已经失去响应了。这个是为什么呢?让我们从代码中取证。
12行定义了一个布尔值的flag变量,这个变量用于标识是否我们还需要更新TextField中的数值,从而让我们感觉是否计时器还在计时。当为false时停
止计时,为true时继续计时——这是程序设计者的原意(你和我很清楚的知道,这个良好的意图泡汤了)。
22-33行是实现计时器功能的核心代码——一个叫做go的方法。在这个方法里面有一个无限的循环,在循环里面先是通过Thread类的静态方法让
当前线
程sleep了100毫秒,之后马上判断我们的flag是否为true,为true则进行TextField的内容更新,为false则停止更新。
35-39为一个内部类,这个内部类其实充当了Start键的监听器的角色。当start键被单击时,这个监听器会被使用,并回调它里面的
actionPerformed
方法。在这个监听器里面的该方法中,直接调用了之前定义的go方法。也就是说,当我们点击start按钮时,go方法会被调用,从而代表了我们的计时器开始
运行了。
41-45为另一个内部类的定义。它是toggle键的监听器类。在这个监听器里面设置了flag的值——将它取反。也就是说,当点击toggle键时就会将flag
从true设为false(或从false设为true),这样go方法中的循环便会在紧接着的一次循环中察觉到这个改变,从而做出停止更新(或开始更新)TextField
的操作,从而看起来像是停止(或开始)了计时工作。
以上的分析从某种意义上来说,是从设计者的理想上面出发的。真正运行这个小程序时你会发现,一旦开始之后,我们的start键、toggle键以及关闭窗
口键都已经失去了响应——情况糟糕透了。为什么会这样呢?
我无法顺利关闭它,也无法停止它——简直是糟透了
还记得刚开始说的那段话么?这个包含简易ui的小程序的ui控制都是包含在一个线程里面的——也就是刚才我们展示的整个代码所被执行的地方。当我们
开始了go方法时,这个线程就专注于处理这一段代码的执行去了。而处在同一个线程中的Button的监听等操作则被“繁忙”的线程“忽略”掉了。而且更糟糕
的是,go方法里面是一个无限循环——也就是说,一旦我们按下了start键开始了go方法的执行,这整个代码被执行的线程便会永远的专注于这个贪婪的go方
法而无暇顾及其他的代码段——无论你是按下start键还是按下toggle键,都没有响应,因为它们不在go的考虑范围之内。你可以想象自己是一个扫描代码的
机器,并且跟着整个代码进行一次“执行”。到后来你会发现自己在go这个地方不断地上下点头,上下点头——这时如果一个朋友告诉你还有start监听的部
分需要你照看,你会很不耐烦的说,go这个地方已经把你完全占用了,你已经无力分身去顾及了。
刚才的代码就是一个典型的线程占用过度的例子。这在很多包含ui的程序——例如android程序——里有体现。例如android新手往往会把一个长时间的
http请求和ui管理放在同一个线程中,这样便导致了ui无法及时响应用户操作,导致用户觉得你的应用很慢——甚至怀疑自己机器的处理能力(-_-)。
那怎么解决刚才的那个问题呢?答案正是本文的主题——多线程机制。
紧接下来请看如下代码:
1 packagecom.william.myclient; 2 3 importjava.awt.*; 4 importjava.awt.event.*; 5 importjava.applet.*; 6 7 classSeparateSubTaskextendsThread{ 8 privateintcount=0; 9 privateCounter2c2; 10 privatebooleanrunFlag=true; 11 12 publicSeparateSubTask(Counter2c2){ 13 this.c2=c2; 14 start(); 15 } 16 17 publicvoidinvertFlag(){ 18 runFlag=!runFlag; 19 } 20 21 publicvoidrun(){ 22 while(true){ 23 try{ 24 sleep(100); 25 } 26 catch(InterruptedExceptione){ 27 28 } 29 if(runFlag) 30 c2.t.setText(Integer.toString(count++)); 31 } 32 } 33 } 34 35 publicclassCounter2extendsApplet{ 36 TextFieldt=newTextField(10); 37 privateSeparateSubTasksp=null; 38 privateButtononOff=newButton("Toggle"),start=newButton("Start"); 39 40 publicvoidinit(){ 41 add(t); 42 start.addActionListener(newStartL()); 43 add(start); 44 onOff.addActionListener(newOnOffL()); 45 add(onOff); 46 } 47 48 classStartLimplementsActionListener{ 49 publicvoidactionPerformed(ActionEvente){ 50 if(sp==null) 51 sp=newSeparateSubTask(Counter2.this); 52 } 53 } 54 55 classOnOffLimplementsActionListener{ 56 publicvoidactionPerformed(ActionEvente){ 57 if(sp!=null) 58 sp.invertFlag(); 59 } 60 } 61 62 publicstaticvoidmain(String[]args){ 63 Counter2applet=newCounter2(); 64 FrameaFrame=newFrame("Counter2"); 65 aFrame.addWindowListener(newWindowAdapter(){ 66 publicvoidwindowClosing(WindowEvente){ 67 System.exit(0); 68 } 69 }); 70 aFrame.add(applet,BorderLayout.CENTER); 71 aFrame.setSize(300,200); 72 applet.init(); 73 applet.start(); 74 aFrame.setVisible(true); 75 } 76 }
正如刚才所说,我们的解决之道是利用java语言特性里面本身就已经包含的多线程机制。
java里面负责多线程的主要有一个接口和一个类。接口就是java.lang.Runable接口,类就是java.lang.Thread类。其实Thread是一个实现了Runa
ble接口的实现类,它包含了Runable接口的一些默认实现以及一些其他的工具类方法(例如开始线程的方法)供开发人员使用。
Runable接口只定义一个方法:void run()。在run方法中的代码段就是我们自定义的线程所要单独执行的代码段。
既然这些都明了了,那我们就去看看刚才的代码。
7-33行是一个继承自Thread类的新的线程类。在这个类里面我们重写了Thread的run方法,从而让我们自己的线程类拥有我们想要的线程行为。8-10为
该线程类所包含的一些必要字段。count就是我们将要不断自增后显示出来的时间值。c2是一个Counter2的引用,负责将我们的线程和Counter2类的示例联
系起来,从而可以让线程类能够更新Counter2示例中的TextField的显示——这段代码见30行。另外在12-15行定义了我们线程类的构造器,我们先将c2进
行了初始化赋值,以将我们即将构造的线程类的示例和某个Counter2示例“挂钩”起来,紧接着我调用了
SeparateSubTask的父类方法start()方法,从而
启动了线程——如此一来,我们构造SeparateSubTask的同时也启动了线程,无需再单独去调用start()方法来启动线程。另外30行利用c2引用来获取Coun
ter2类实例中的TextField示例引用,从而改变TextField的显示的手法也是我们需要理解的。
35-76行我们定义了一个Counter2类,这个类和刚才的Counter1类无多大差别——除了37行我们保存了一个需要利用到的SeparateSubTask的引用以
及50-51行我们将这个引用指向了一个新构建的SeparateSubTask类实例(该新线程实例构建即马上运行)。
通过以上代码,我们便可以保证按钮响应顺利的同时,TextField的显示更新同时顺利进行的效果。
当我按下Toggle键时,数字很听话地停在了52.
为什么会这样呢?思考后我们发现,ui管理的线程和计时更新的线程通过刚才的代码被成功地分配到两个独立的线程中去了——一个是Counter2所在线
程,另一个是SeparateSubTask所在线程——它们两个互不影响,各自做着各自的事情。
另外你会发现,刚才那个代码很好的将界面和功能进行了分模块化构建——较为符合MVC的设计思想。
由于方才的SeparateSubTask和Counter2类之间存在着相互引用的“亲密”关系,所以我们不妨就让它们成为一家子——让SeparateSubTask成为Co
unter2的内部类。相关代码如下:
1 packagecom.william.myclient; 2 3 importjava.awt.*; 4 importjava.awt.event.*; 5 importjava.applet.*; 6 7 publicclassCounter2iextendsApplet{ 8 privateclassSeparateSubTaskextendsThread{ 9 intcount=0; 10 booleanrunFlag=true; 11 12 SeparateSubTask(){ 13 start(); 14 } 15 16 publicvoidrun(){ 17 while(true){ 18 try{ 19 sleep(100); 20 }catch(InterruptedExceptione){ 21 22 } 23 if(runFlag) 24 t.setText(Integer.toString(count++)); 25 } 26 } 27 } 28 29 privateSeparateSubTasksp=null; 30 privateTextFieldt=newTextField(10); 31 privateButtononOff=newButton("Toggle"),start=newButton("Start"); 32 33 publicvoidinit(){ 34 add(t); 35 start.addActionListener(newStartL()); 36 add(start); 37 onOff.addActionListener(newOnOffL()); 38 add(onOff); 39 } 40 41 classStartLimplementsActionListener{ 42 publicvoidactionPerformed(ActionEvente){ 43 if(sp==null) 44 sp=newSeparateSubTask(); 45 } 46 } 47 48 classOnOffLimplementsActionListener{ 49 publicvoidactionPerformed(ActionEvente){ 50 if(sp!=null) 51 sp.runFlag=!sp.runFlag;//invertFlag(); 52 } 53 } 54 55 publicstaticvoidmain(String[]args){ 56 Counter2iapplet=newCounter2i(); 57 FrameaFrame=newFrame("Counter2i"); 58 aFrame.addWindowListener(newWindowAdapter(){ 59 publicvoidwindowClosing(WindowEvente){ 60 System.exit(0); 61 } 62 }); 63 aFrame.add(applet,BorderLayout.CENTER); 64 aFrame.setSize(300,200); 65 applet.init(); 66 applet.start(); 67 aFrame.setVisible(true); 68 } 69 } 70
这份代码和刚才的代码运行效果是一致的——不信你试试。需要提醒的是,当两个类之间的耦合关系确实很是紧密的时候,内部类往往是一个不错的选
择。
另外还有另一种形式的处理方法:将线程类和ui控制类组合到一起。具体手段则是构建一个Counter3类,让它继承自Applet类并且实现Runable接口
(java中是禁止多继承的)。
实现代码如下:
1 packagecom.william.myclient; 2 3 importjava.awt.*; 4 importjava.awt.event.*; 5 importjava.applet.*; 6 7 publicclassCounter3extendsAppletimplementsRunnable{ 8 privateintcount=0; 9 privatebooleanrunFlag=true; 10 privateThreadselfThread=null; 11 privateButtononOff=newButton("Toggle"),start=newButton("Start"); 12 privateTextFieldt=newTextField(10); 13 14 publicvoidinit(){ 15 add(t); 16 start.addActionListener(newStartL()); 17 add(start); 18 onOff.addActionListener(newOnOffL()); 19 add(onOff); 20 } 21 22 publicvoidrun(){ 23 while(true){ 24 try{ 25 selfThread.sleep(100); 26 }catch(InterruptedExceptione){ 27 } 28 if(runFlag) 29 t.setText(Integer.toString(count++)); 30 } 31 } 32 33 classStartLimplementsActionListener{ 34 publicvoidactionPerformed(ActionEvente){ 35 if(selfThread==null){ 36 selfThread=newThread(Counter3.this); 37 selfThread.start(); 38 } 39 } 40 } 41 42 classOnOffLimplementsActionListener{ 43 publicvoidactionPerformed(ActionEvente){ 44 runFlag=!runFlag; 45 } 46 } 47 48 publicstaticvoidmain(String[]args){ 49 Counter3applet=newCounter3(); 50 FrameaFrame=newFrame("Counter3"); 51 aFrame.addWindowListener(newWindowAdapter(){ 52 publicvoidwindowClosing(WindowEvente){ 53 System.exit(0); 54 } 55 }); 56 aFrame.add(applet,BorderLayout.CENTER); 57 aFrame.setSize(300,200); 58 applet.init(); 59 applet.start(); 60 aFrame.setVisible(true); 61 } 62 } 63
从代码中我们可以看出如下玄机:
10行为一个Thread类的引用,这个引用用于将Counter3类的Runable接口的实现进行封装后便于启动线程——具体代码在36-37行的代码。其他地方便
和之前的几个Counter无多大出入。另外有一点需要提醒:这个Counter3中只能实现一个计时线程的运行——因为通过这个代码的实现我们已经将计时部分和
ui管理部分进行极为高度的结合,不可能实现它们两个的分离实例化了。从这点看,前面几个基本上都可以实现多个计时部分的示例化,从而为多计时器的实
现留下了余地。
最后我们就给大家看一个多计时器的例子:
1 packagecom.william.myclient; 2 3 importjava.awt.*; 4 importjava.awt.event.*; 5 importjava.applet.*; 6 7 classTickerextendsThread{ 8 privateButtonb=newButton("Toggle"); 9 privateTextFieldt=newTextField(10); 10 privateintcount=0; 11 privatebooleanrunFlag=true; 12 13 publicTicker(Containerc){ 14 b.addActionListener(newToggleL()); 15 Panelp=newPanel(); 16 p.add(t); 17 p.add(b); 18 c.add(p); 19 } 20 21 classToggleLimplementsActionListener{ 22 publicvoidactionPerformed(ActionEvente){ 23 runFlag=!runFlag; 24 } 25 } 26 27 publicvoidrun(){ 28 while(true){ 29 if(runFlag) 30 t.setText(Integer.toString(count++)); 31 try{ 32 sleep(100); 33 }catch(InterruptedExceptione){ 34 } 35 } 36 } 37 } 38 39 publicclassCounter4extendsApplet{ 40 privateButtonstart=newButton("Start"); 41 privatebooleanstarted=false; 42 privateTicker[]s; 43 privatebooleanisApplet=true; 44 privateintsize; 45 46 publicvoidinit(){ 47 //Getparameter"size"fromWebpage: 48 if(isApplet) 49 size=Integer.parseInt(getParameter("size")); 50 s=newTicker[size]; 51 for(inti=0;i<s.length;i++) 52 s[i]=newTicker(this); 53 start.addActionListener(newStartL()); 54 add(start); 55 } 56 57 classStartLimplementsActionListener{ 58 publicvoidactionPerformed(ActionEvente){ 59 if(!started){ 60 started=true; 61 for(inti=0;i<s.length;i++) 62 s[i].start(); 63 } 64 } 65 } 66 67 publicstaticvoidmain(String[]args){ 68 Counter4applet=newCounter4(); 69 //Thisisn'tanapplet,sosettheflagand 70 //producetheparametervaluesfromargs: 71 applet.isApplet=false; 72 applet.size=(args.length==0?5:Integer.parseInt(args[0])); 73 FrameaFrame=newFrame("Counter4"); 74 aFrame.addWindowListener(newWindowAdapter(){ 75 publicvoidwindowClosing(WindowEvente){ 76 System.exit(0); 77 } 78 }); 79 aFrame.add(applet,BorderLayout.CENTER); 80 aFrame.setSize(200,applet.size*50); 81 applet.init(); 82 applet.start(); 83 aFrame.setVisible(true); 84 } 85 } 86
具体运行效果如下:
好啦,就到这里吧。过年了,大年初一写了这篇博文。在这里祝大家兔年吉祥咯!