在进入实战篇以前,我们简单说一下多线程编程的一般原则。
[安全性]是多线程编程的首要原则,如果两个以上的线程访问同一对象时,一个线程会损坏另一个线程的数据,这就是违反了安全性原则,这样的程序是不能进入实际应用的。
安全性的保证可以通过设计安全的类和程序员的手工控制。如果多个线程对同一对象访问不会危及安全性,这样的类就是线程安全的类,在JAVA中比如String类就被设计为线程安全的类。而如果不是线程安全的类,那么就需要程序员在访问这些类的实例时手工控制它的安全性。
[可行性]是多线程编程的另一个重要原则,如果仅仅实现了安全性,程序却在某一点后不能继续执行或者多个线程发生死锁,那么这样的程序也不能作为真正的多线程程序来应用。
相对而言安全性和可行性是相互抵触的,安全性越高的程序,可性行会越低。要综合平衡。
[高性能] 多线程的目的本来就是为了增加程序运行的性能,如果一个多线程完成的工作还不如单线程完成得快。那就不要应用多线程了。
高性能程序主要有以下几个方面的因素:
数据吞吐率,在一定的时间内所能完成的处理能力。
响应速度,从发出请求到收到响应的时间。
容量,指同时处理雅致同任务的数量。
安全性和可行性是必要条件,如果达到不这两个原则那就不能称为真正的多线程程序。而高性是多线程编程的目的,也可以说是充要条件。否则,为什么采用多线程编程呢?
[生产者与消费者模式]
首先以一个生产者和消费者模式来进入实战篇的第一节。
生产者和消费者模式中保护的是谁?
多线程编程都在保护着某些对象,这些个对象是"紧俏资源",要被最大限度地利用,这也是采用多线程方式的理由。在生产者消费者模式中,我们要保护的是"仓库",在我下面的这个例子中,
就是桌子(table)。
我这个例子的模式完全是生产者-消费者模式,但我换了个名字。厨师-食客模式,这个食堂中只有1张桌子,同时最多放10个盘子,现在有4个厨师做菜,每做好一盘就往桌子上放(生产者将产品往仓库中放),而有6个食客不停地吃(消费者消费产品,为了说明问题,他们的食量是无限的)。
一般而言,厨师200-400ms做出一盘菜,而食客要400-600ms吃完一盘。当桌子上放满了10个盘子后,所有厨师都不能再往桌子上放,而当桌子是没有盘子时,所有的食客都只好等待。
下面我们来设计这个程序:
因为我们不知道具体是什么菜,所以叫它food:
class Food{}
然后是桌子,因为它要有序地放而且要有序地取(不能两个食客同时争取第三盘菜),所以我们扩展LinkedList,或者你用聚合把一个LinkedList作为属性也能达到同样的目的,例子中我是用
继承,从构造方法中传入一个可以放置的最大值。
class Table extends java.util.LinkedList{ int maxSize; public Table(int maxSize){ this.maxSize = maxSize; } }
现在我们要为它加两个方法,一是厨师往上面放菜的方法,一是食客从桌子上拿菜的方法。
放菜:因为一张桌子由多个厨师放菜,所以厨师放菜的要被同步,如果桌子上已经有十盘菜了。所有厨师就要等待:
public synchronized void putFood(Food f){ while(this.size() >= this.maxSize){ try{ this.wait(); }catch(Exception e){} } this.add(f); notifyAll(); }
拿菜:同上面,如果桌子上一盘菜也没有,所有食客都要等待:
public synchronized Food getFood(){ while(this.size() <= 0){ try{ this.wait(); }catch(Exception e){} } Food f = (Food)this.removeFirst(); notifyAll(); return f; }
厨师类:
由于多个厨师要往一张桌子上放菜,所以他们要操作的桌子应该是同一个对象,我们从构造方法中将桌子对象传进去以便控制在主线程中只产生一张桌子。
厨师做菜要用一定的时候,我用在make方法中用sleep表示他要消耗和时候,用200加上200的随机数保证时间有200-400ms中。做好后就要往桌子上放。
这里有一个非常重要的问题一定要注意,就是对什么范围同步的问题,因为产生竞争的是桌子,所以所有putFood是同步的,而我们不能把厨师自己做菜的时间也放在同步中,因为做菜是各自做的。同样食客吃菜的时候也不应该同步,只有从桌子中取菜的时候是竞争的,而具体吃的时候是各自在吃。所以厨师类的代码如下:
class Chef extends Thread{ Table t; Random r = new Random(12345); public Chef(Table t){ this.t = t; } public void run(){ while(true){ Food f = make(); t.putFood(f); } } private Food make(){ try{ Thread.sleep(200+r.nextInt(200)); }catch(Exception e){} return new Food(); } }
同理我们产生食客类的代码如下:
class Eater extends Thread{ Table t; Random r = new Random(54321); public Eater(Table t){ this.t = t; } public void run(){ while(true){ Food f = t.getFood(); eat(f); } } private void eat(Food f){ try{ Thread.sleep(400+r.nextInt(200)); }catch(Exception e){} } }
完整的程序在这儿:
package debug; import java.util.regex.*; import java.util.*; class Food{} class Table extends LinkedList{ int maxSize; public Table(int maxSize){ this.maxSize = maxSize; } public synchronized void putFood(Food f){ while(this.size() >= this.maxSize){ try{ this.wait(); }catch(Exception e){} } this.add(f); notifyAll(); } public synchronized Food getFood(){ while(this.size() <= 0){ try{ this.wait(); }catch(Exception e){} } Food f = (Food)this.removeFirst(); notifyAll(); return f; } } class Chef extends Thread{ Table t; String name; Random r = new Random(12345); public Chef(String name,Table t){ this.t = t; this.name = name; } public void run(){ while(true){ Food f = make(); System.out.println(name+" put a Food:"+f); t.putFood(f); } } private Food make(){ try{ Thread.sleep(200+r.nextInt(200)); }catch(Exception e){} return new Food(); } } class Eater extends Thread{ Table t; String name; Random r = new Random(54321); public Eater(String name,Table t){ this.t = t; this.name = name; } public void run(){ while(true){ Food f = t.getFood(); System.out.println(name+" get a Food:"+f); eat(f); } } private void eat(Food f){ try{ Thread.sleep(400+r.nextInt(200)); }catch(Exception e){} } } public class Test { public static void main(String[] args) throws Exception{ Table t = new Table(10); new Chef("Chef1",t).start(); new Chef("Chef2",t).start(); new Chef("Chef3",t).start(); new Chef("Chef4",t).start(); new Eater("Eater1",t).start(); new Eater("Eater2",t).start(); new Eater("Eater3",t).start(); new Eater("Eater4",t).start(); new Eater("Eater5",t).start(); new Eater("Eater6",t).start(); } }
这一个例子中,我们主要关注以下几个方面:
1.同步方法要保护的对象,本例中是保护桌子,不能同时往上放菜或同时取菜。
假如我们把putFood方法和getFood方法在厨师类和食客类中实现,那么我们应该如此:
(以putFood为例)
class Chef extends Thread{ Table t; String name; public Chef(String name,Table t){ this.t = t; this.name = name; } public void run(){ while(true){ Food f = make(); System.out.println(name+" put a Food:"+f); putFood(f); } } private Food make(){ Random r = new Random(200); try{ Thread.sleep(200+r.nextInt()); }catch(Exception e){} return new Food(); } public void putFood(Food f){//方法本身不能同步,因为它同步的是this.即Chef的实例 synchronized (t) {//要保护的是t while (t.size() >= t.maxSize) { try { t.wait(); } catch (Exception e) {} } t.add(f); t.notifyAll(); } } }
2.同步的范围,在本例中是放和取两个方法,不能把做菜和吃菜这种各自不相干的工作放在受保护的范围中。
3.参与者与容积比
对于生产者和消费者的比例,以及桌子所能放置最多菜的数量三者之间的关系是影响性能的重要因素,如果是过多的生产者在等待,则要增加消费者或减少生产者的数据,反之则增加生产者或减少消费者的数量。
另外如果桌子有足够的容量可以很大程序提升性能,这种情况下可以同时提高生产者和消费者的数量,但足够大的容时往往你要有足够大的物理内存。
多线程编程——实战篇(二)
本节继续上一节的讨论。
[一个线程在进入对象的休息室(调用该对象的wait()方法)后会释放对该对象的锁],基于这个原因。在同步中,除非必要,否则你不应用使用Thread.sleep(long l)方法,因为sleep方法并不释放对象的锁。
这是一个极其恶劣的品德,你自己什么事也不干,进入sleep状态,却抓住竞争对象的监视锁不让其它需要该对象监视锁的线程运行,简单说是极端自私的一种行为。但我看到过很多程序员仍然有在同步方法中调用sleep的代码。
看下面的例子:
package debug; class SleepTest{ public synchronized void wantSleep(){ try{ Thread.sleep(1000*60); }catch(Exception e){} System.out.println("111"); } public synchronized void say(){ System.out.println("123"); } } class T1 extends Thread{ SleepTest st; public T1(SleepTest st){ this.st = st; } public void run(){ st.wantSleep(); } } class T2 extends Thread{ SleepTest st; public T2(SleepTest st){ this.st = st; } public void run(){ st.say(); } } public class Test { public static void main(String[] args) throws Exception{ SleepTest st = new SleepTest(); new T1(st).start(); new T2(st).start(); } }
我们看到,线程T1的实例运行后,当前线程抓住了st实例的锁,然后进入了sleep。直到它睡满60秒后才运行到System.out.println("111");然后run方法运行完成释放了对st的监视锁,线程T2的实例才得到运行的机会。
而如果我们把wantSleep方法改成:
public synchronized void wantSleep(){ try{ //Thread.sleep(1000*60); this.wait(1000*60); }catch(Exception e){} System.out.println("111"); }
我们看到,T2的实例所在的线程立即就得到了运行机会,首先打印了123,而T1的实例所在的线程仍然等待,直到等待60秒后运行到System.out.println("111");方法。
所以,调用wait(long l)方法不仅达到了阻塞当前线程规定时间内不运行,而且让其它有竞争需求的线程有了运行机会,这种利人不损己的方法,何乐而不为?这也是一个有良心的程序员应该遵循的原则。
当一个线程调用wait(long l)方法后,线程如果继续运行,你无法知道它是等待时间完成了还是在wait时被其它线程唤醒了,如果你非常在意它一定要等待足够的时间才执行某任务,而不希望是中途被唤醒,这里有一个不是非常准确的方法:
long l = System.System.currentTimeMillis(); wait(1000);//准备让当前线程等待1秒 while((System.System.currentTimeMillis() - l) < 1000)//执行到这里说明它还没有等待到1秒 //是让其它线程给闹醒了 wait(1000-(System.System.currentTimeMillis()-l));//继续等待余下的时间.
这种方法不是很准确,但基本上能达到目的。
所以在同步方法中,除非你明确知道自己在干什么,非要这么做的话,你没有理由使用sleep,wait方法足够达到你想要的目的。而如果你是一个很保守的人,看到上面这段话后,你对sleep方法深恶痛绝,坚决不用sleep了,那么在非同步的方法中(没有和其它线程竞争的对象),你想让当前线程阻塞一定时间后再运行,应该如何做呢?(这完全是一种卖弄,在非同步的方法中你就应该合理地应用sleep嘛,但如果你坚决不用sleep,那就这样来做吧)
public static mySleep(long l){ Object o = new Object(); synchronized(o){ try{ o.wait(l); }catch(Exception e){} } }
放心吧,没有人能在这个方法外调用o.notify[All],所以o.wait(l)会一直等到设定的时间才会运行完成。
虚拟锁简单说就是不要调用synchronized方法(它等同于synchronized(this))和不要调用synchronized(this),这样所有调用在这个实例上的所有同步方法的线程只能有一个线程可以运行。也就是说:
如果一个类有两个同步方法 m1,m2,那么不仅是两个以上线调用m1方法的线程只有一个能运行,就是两个分别调用m1,m2的线程也只有一个能运行。当然非同步方法不存在任何竞争,在一个线程获取该对象的监视锁后这个对象的非同步方法可以被任何线程调用。
而大多数时候,我们可能会出现这种情况,多个线程调用m1时需要保护一种资源,而多个线程调用M2时要保护的是另一种资源,如果我们把m1,m2都设成同步方法。两个分别调用这两个方法的线程其实并不产生冲突,但它们都要获取这个实例的锁(同步方法是同步this)而产生了不必要竞争。
所以这里应该采用虚拟锁。
即将m1和m2方法中各自保护的对象作为属性a1,a2传进来,然后将同步方法改为方法的同步块分别以a1,a2为参数,这样到少是不同线程调用这两个不同方法时不会产生竞争,当然如果m1,m2方法都操作同一受保护对象则两个方法还是应该作为同步方法。这也是应该将方法同步还是采用同步块的理由之一。
package debug; class SleepTest{ public synchronized void m1(){ System.out.println("111"); try{ Thread.sleep(10000); }catch(Exception e){} } public synchronized void m2(){ System.out.println("123"); } } class T1 extends Thread{ SleepTest st; public T1(SleepTest st){ this.st = st; } public void run(){ st.m1(); } } class T2 extends Thread{ SleepTest st; public T2(SleepTest st){ this.st = st; } public void run(){ st.m2(); } } public class Test { public static void main(String[] args) throws Exception{ SleepTest st = new SleepTest(); new T1(st).start(); new T2(st).start(); } }
这个例子可以看到两个线程分别调用st实例的m1和m2方法却因为都要获取st的监视锁而产生了竞争。T2实例要在T1运行完成后才能运行(间隔了10秒)。而假设m1方法要操作操作一个文件 f1,m2方法要操作一个文件f2,当然我们可以在方法中分别同步f1,f2,但现在还不知道f2,f2是否存在,如果不存在我们就同步了一个null对象,那么我们可以使用虚拟锁:
package debug; class SleepTest{ String vLock1 = "vLock1"; String vLock2 = "vLock2"; public void m1(){ synchronized(vLock1){ System.out.println("111"); try { Thread.sleep(10000); } catch (Exception e) {} //操作f1 } } public void m2(){ synchronized(vLock2){ System.out.println("123"); //操作f2 } } } class T1 extends Thread{ SleepTest st; public T1(SleepTest st){ this.st = st; } public void run(){ st.m1(); } } class T2 extends Thread{ SleepTest st; public T2(SleepTest st){ this.st = st; } public void run(){ st.m2(); } } public class Test { public static void main(String[] args) throws Exception{ SleepTest st = new SleepTest(); new T1(st).start(); new T2(st).start(); } }
我们看到两个分别调用m1和m2的线程由于它们获取不同对象的监视锁,它们没有任何竞争就正常运行,只有这两个线程同时调用m1或m2才会产生阻塞。
多线程编程——实战篇(三)
在基础篇中的第一节,我就强调过,要了解多线程编程,首要的两个概念就是线程对象和线程。现在我们来深入理解线程对象,线程,运行环境之间的关系,弄清Runnable与Thread的作用。
在JAVA平台中,序列化机制是一个非常重要的机制,如果不能理解并熟练应用序列化机制,你就不能称得上一个java程序员。
在JAVA平台中,为什么有些对象中可序列化的,而有些对象就不能序列化?
能序列化的对象,简单说是一种可以复制(意味着可以按一定机制进行重构它)的对象,这种对象说到底就是内存中一些数据的组合。只要按一定位置和顺序组合就能完整反映这个对象。
而有些对象,是和当前环境相关的,它反映了当前运行的环境和时序,所以不能被序列,否则在另外的环境和时序中就无法“还原”。
比如,一个Socket对象:
Socket sc = new Socket("111.111.111.111",80);
这个sc对象表示当前正在运行这段代码的主机和IP为"111.111.111.111"的80端口之间建立的一个物理连结,如果它被序列化,那么在另一个时刻在另一个主机上它如何能被还原?Socket连结一旦断开,就已经不存在,它不可能在另一个时间被另一个主机所重现。重现的已经不是原来那个sc对象了。
线程对象也是这种不可序列化对象,当我们new Thread时,已经初始化了当前这个线程对象所在有主机的运行环境相关的信息,线程调度机制,安全机制等只特定于当前运行环境的信息,假如它被序列化,在另一个环境中运行的时候原来初始化的运行环境的信息就不可能在新的环境中运行。而假如要重新初始化,那它已经不是原来那个线程对象了。
正如Socket封装了两个主机之间的连结,但它们并不是已经连结关传送数据了。要想传送数据,你还要getInputStream和getOutputStream,并read和write,两台主机之间才开始真正的“数据连结”。
一个Thread对象并建立后,只是有了可以"运行"的令牌,仅仅只是一个"线程对象"。只有当它调用start()后,当前环境才会分配给它一个运行的"空间",让这段代码开始运行。这个运行的"空间",才叫真正的"线程"。也就是说,真正的线程是指当前正在执行的那一个"事件"。是那个线程对象所在的运行环境。
明白了上面的概念,我们再来看看JAVA中为什么要有Runnable对象和Thread对象。
一、从设计技巧上说,JAVA中为了实现回调,无法调用方法指针,那么利用接口来约束实现者强制提供匹配的方法,并将实现该接口的类的实例作为参数来提供给调用者,这是JAVA平台实现回调的重要手段。
二、但是从实际的操作来看,对于算法和数据,是不依赖于任何环境的。所以把想要实现的操作中的算法和数据封装到一个run方法中(由于算法本身是数据的一个部分,所以我把它们合并称为数据),可以将离数据和环境的逻辑分离开来。使程序员只关心如何实现我想做的操作,而不要关心它所在的环境。当真正的需要运行的时候再将这段"操作"传给一个具体当前环境的Thread对象。
三、这是最最重要的原因:实现数据共享
因为一个线程对象不对多次运行。所以把数据放在Thread对象中,不会被多个线程同时访问。简单说:
class T extends Thread{ Object x; public void run(){//......;} } T t = new T();
当T的实例t运行后,t所包含的数据x只能被一个t.start();对象共享,除非声明成 static Object x;
一个t的实例数据只能被一个线程访问。意思是"一个数据实例对应一个线程"。
而假如我们从外部传入数据,比如
class T extends Thread{ private Object x; public T(Object x){ this.x = x; } public void run(){//......;} }
这样我们就可以先生成一个x对象传给多个Thread对象,多个线程共同操作一个数据。也就是"一个数据实例对应多个线程"。
现在我们把数据更好地组织一下,把要操作的数据Object x和要进行的操作一个封装到Runnable的run()方法中,把Runnable实例从外部传给多个Thread对象。这样,我们就有了:
[一个对象的多个线程]
这是以后我们要介绍的线程池的重要概念。
多线程编程——实战篇(四)
不客气地说,至少有一半人认为,线程的“中断”就是让线程停止。如果你也这么认为,那你对多线程编程还没有入门。
在java中,线程的中断(interrupt)只是改变了线程的中断状态,至于这个中断状态改变后带来的结果,那是无法确定的,有时它更是让停止中的线程继续执行的唯一手段。不但不是让线程停止运行,反而是继续执行线程的手段。
对于执行一般逻辑的线程,如果调用它的interrupt()方法,那么对这个线程没有任何影响,比如线程a正在执行:while(条件) x ++;这样的语句,如果其它线程调用a.interrupt();那么并不会影响a对象上运行的线程,如果在其它线程里测试a的中断状态它已经改变,但并不会停止这个线程的运行。在一个线程对象上调用interrupt()方法,真正有影响的是wait,join,sleep方法,当然这三个方法包括它们的重载方法。
请注意:[上面这三个方法都会抛出InterruptedException],记住这句话,下面我会重复。一个线程在调用interrupt()后,自己不会抛出InterruptedException异常,所以你看到interrupt()并没有抛出这个异常,所以我上面说如果线程a正在执行while(条件) x ++;你调用a.interrupt();后线程会继续正常地执行下去。
但是,如果一个线程被调用了interrupt()后,它的状态是已中断的。这个状态对于正在执行wait,join,sleep的线程,却改变了线程的运行结果。
一、对于wait中等待notify/notifyAll唤醒的线程,其实这个线程已经“暂停”执行,因为它正在某一对象的休息室中,这时如果它的中断状态被改变,那么它就会抛出异常。这个InterruptedException异常不是线程抛出的,而是wait方法,也就是对象的wait方法内部会不断检查在此对象上休息的线程的状态,如果发现哪个线程的状态被置为已中断,则会抛出InterruptedException,意思就是这个线程不能再等待了,其意义就等同于唤醒它了。
这里唯一的区别是,被notify/All唤醒的线程会继续执行wait下面的语句,而在wait中被中断的线程则将控制权交给了catch语句。一些正常的逻辑要被放到catch中来运行。但有时这是唯一手段,比如一个线程a在某一对象b的wait中等待唤醒,其它线程必须获取到对象b的监视锁才能调用b.notify()[All],否则你就无法唤醒线程a,但在任何线程中可以无条件地调用a.interrupt();来达到这个目的。只是唤醒后的逻辑你要放在catch中,当然同notify/All一样,继续执行a线程的条件还是要等拿到b对象的监视锁。
二、对于sleep中的线程,如果你调用了Thread.sleep(一年);现在你后悔了,想让它早些醒过来,调用interrupt()方法就是唯一手段,只有改变它的中断状态,让它从sleep中将控制权转到处理异常的catch语句中,然后再由catch中的处理转换到正常的逻辑。同样地,于join中的线程你也可以这样处理。
对于一般介绍多线程模式的书上,他们会这样来介绍:当一个线程被中断后,在进入wait,sleep,join方法时会抛出异常。是的,这一点也没有错,但是这有什么意义呢?如果你知道那个线程的状态已经处于中断状态,为什么还要让它进入这三个方法呢?当然有时是必须这么做的,但大多数时候没有这么做的理由,所以我上面主要介绍了在已经调用这三个方法的线程上调用interrupt()方法让它从"暂停"状态中恢复过来。这个恢复过来就可以包含两个目的:
一、[可以使线程继续执行],那就是在catch语句中招待醒来后的逻辑,或由catch语句转回正常的逻辑。总之它是从wait,sleep,join的暂停状态活过来了。
二、[可以直接停止线程的运行],当然在catch中什么也不处理,或return,那么就完成了当前线程的使命,可以使在上面“暂停”的状态中立即真正的“停止”。
中断线程
有了上一节[线程的中断],我们就好进行如何[中断线程]了。这绝对不是玩一个文字游戏。是因为“线程的中断”并不能保证“中断线程”,所以我要特别地分为两节来说明。这里说的“中断线程”意思是“停止线程”,而为什么不用“停止线程”这个说法呢?因为线程有一个明确的stop方法,但它是反对使用的,所以请大家记住,在java中以后不要提停止线程这个说法,忘记它!但是,作为介绍线程知识的我,我仍然要告诉你为什么不用“停止线程”的理由。
[停止线程]
当在一个线程对象上调用stop()方法时,这个线程对象所运行的线程就会立即停止,并抛出特殊的ThreadDeath()异常。这里的“立即”因为太“立即”了,就象一个正在摆弄自己的玩具的孩子,听到大人说快去睡觉去,就放着满地的玩具立即睡觉去了。这样的孩子是不乖的。
假如一个线程正在执行:
synchronized void { x = 3; y = 4; }
由于方法是同步的,多个线程访问时总能保证x,y被同时赋值,而如果一个线程正在执行到x = 3;时,被调用了 stop()方法,即使在同步块中,它也干脆地stop了,这样就产生了不完整的残废数据。而多线程编程中最最基础的条件要保证数据的完整性,所以请忘记线程的stop方法,以后我们再也不要说“停止线程”了。
如何才能“结束”一个线程?
[中断线程]
结束一个线程,我们要分析线程的运行情况。也就是线程正在干什么。如果那个孩子什么事也没干,那就让他立即去睡觉。而如果那个孩子正在摆弄他的玩具,我们就要让它把玩具收拾好再睡觉。
所以一个线程从运行到真正的结束,应该有三个阶段:
在我的JDBC专栏中我N次提醒在一个SQL逻辑结束后,无论如何要保证关闭Connnection那就是在finally从句中进行。同样,线程在结束前的工作应该在finally中来保证线程退出前一定执行:
try{ 正在逻辑 }catch(){} finally{ 清理工作 }
那么如何让一个线程结束呢?既然不能调用stop,可用的只的interrupt()方法。但interrupt()方法只是改变了线程的运行状态,如何让它退出运行?对于一般逻辑,只要线程状态已经中断,我们就可以让它退出,所以这样的语句可以保证线程在中断后就能结束运行:
while(!isInterrupted()){ 正常逻辑 }
这样如果这个线程被调用interrupt()方法,isInterrupted()为true,就会退出运行。但是如果线程正在执行wait,sleep,join方法,你调用interrupt()方法,这个逻辑就不完全了。
如果一个有经验的程序员来处理线程的运行的结束:
public void run(){ try{ while(!isInterrupted()){ 正常工作 } } catch(Exception e){ return; } finally{ 清理工作 } }
我们看到,如果线程执行一般逻辑在调用innterrupt后,isInterrupted()为true,退出循环后执行清理工作后结束,即使线程正在wait,sleep,join,也会抛出异常执行清理工作后退出。
这看起来非常好,线程完全按最我们设定的思路在工作。但是,并不是每个程序员都有这种认识,如果他聪明的自己处理异常会如何?事实上很多或大多数程序员会这样处理:
public void run(){ while(!isInterrupted()){ try{ 正常工作 }catch(Exception e){ //nothing } finally{ } } } }
想一想,如果一个正在sleep的线程,在调用interrupt后,会如何?wait方法检查到isInterrupted()为true,抛出异常,而你又没有处理。而一个抛出了InterruptedException的线程的状态马上就会被置为非中断状态,如果catch语句没有处理异常,则下一次循环中isInterrupted()为false,线程会继续执行,可能你N次抛出异常,也无法让线程停止。
那么如何能确保线程真正停止?在线程同步的时候我们有一个叫“二次惰性检测”(double check),能在提高效率的基础上又确保线程真正中同步控制中。那么我把线程正确退出的方法称为“双重安全退出”,即不以isInterrupted()为循环条件。而以一个标记作为循环条件:
class MyThread extend Thread{ private boolean isInterrupted = false;//这一句以后要修改 public void interrupt(){ isInterrupted = true; super.interrupt(); } public void run(){ while(!isInterrupted){ try{ 正常工作 }catch(Exception e){ //nothing } finally{ } } } }
试试这段程序,可以正确工作吗?
对于这段程序仍然还有很多可说的地方,先到这里吧。
转载自dev2dev网友axman的go deep into java专栏。