時(shí)間:2006-09-12
作者:axman
在進(jìn)入實(shí)戰(zhàn)篇以前,我們簡(jiǎn)單說一下多線程編程的一般原則。
[安全性]是多線程編程的首要原則,如果兩個(gè)以上的線程訪問同一對(duì)象時(shí),一個(gè)線程會(huì)損壞另一個(gè)線程的數(shù)據(jù),這就是違反了安全性原則,這樣的程序是不能進(jìn)入實(shí)際應(yīng)用的。
安全性的保證可以通過設(shè)計(jì)安全的類和程序員的手工控制。如果多個(gè)線程對(duì)同一對(duì)象訪問不會(huì)危及安全性,這樣的類就是線程安全的類,在JAVA中比如String類就被設(shè)計(jì)為線程安全的類。而如果不是線程安全的類,那么就需要程序員在訪問這些類的實(shí)例時(shí)手工控制它的安全性。
[可行性]是多線程編程的另一個(gè)重要原則,如果僅僅實(shí)現(xiàn)了安全性,程序卻在某一點(diǎn)后不能繼續(xù)執(zhí)行或者多個(gè)線程發(fā)生死鎖,那么這樣的程序也不能作為真正的多線程程序來應(yīng)用。
相對(duì)而言安全性和可行性是相互抵觸的,安全性越高的程序,可性行會(huì)越低。要綜合平衡。
[高性能] 多線程的目的本來就是為了增加程序運(yùn)行的性能,如果一個(gè)多線程完成的工作還不如單線程完成得快。那就不要應(yīng)用多線程了。
高性能程序主要有以下幾個(gè)方面的因素:
數(shù)據(jù)吞吐率,在一定的時(shí)間內(nèi)所能完成的處理能力。
響應(yīng)速度,從發(fā)出請(qǐng)求到收到響應(yīng)的時(shí)間。
容量,指同時(shí)處理雅致同任務(wù)的數(shù)量。
安全性和可行性是必要條件,如果達(dá)到不這兩個(gè)原則那就不能稱為真正的多線程程序。而高性是多線程編程的目的,也可以說是充要條件。否則,為什么采用多線程編程呢?
[生產(chǎn)者與消費(fèi)者模式]
首先以一個(gè)生產(chǎn)者和消費(fèi)者模式來進(jìn)入實(shí)戰(zhàn)篇的第一節(jié)。
生產(chǎn)者和消費(fèi)者模式中保護(hù)的是誰?
多線程編程都在保護(hù)著某些對(duì)象,這些個(gè)對(duì)象是"緊俏資源",要被最大限度地利用,這也是采用多線程方式的理由。在生產(chǎn)者消費(fèi)者模式中,我們要保護(hù)的是"倉庫",在我下面的這個(gè)例子中,
就是桌子(table)。
我這個(gè)例子的模式完全是生產(chǎn)者-消費(fèi)者模式,但我換了個(gè)名字。廚師-食客模式,這個(gè)食堂中只有1張桌子,同時(shí)最多放10個(gè)盤子,現(xiàn)在有4個(gè)廚師做菜,每做好一盤就往桌子上放(生產(chǎn)者將產(chǎn)品往倉庫中放),而有6個(gè)食客不停地吃(消費(fèi)者消費(fèi)產(chǎn)品,為了說明問題,他們的食量是無限的)。
一般而言,廚師200-400ms做出一盤菜,而食客要400-600ms吃完一盤。當(dāng)桌子上放滿了10個(gè)盤子后,所有廚師都不能再往桌子上放,而當(dāng)桌子是沒有盤子時(shí),所有的食客都只好等待。
下面我們來設(shè)計(jì)這個(gè)程序:
因?yàn)槲覀儾恢谰唧w是什么菜,所以叫它food:
class Food{}
然后是桌子,因?yàn)樗行虻胤哦乙行虻厝?不能兩個(gè)食客同時(shí)爭(zhēng)取第三盤菜),所以我們擴(kuò)展LinkedList,或者你用聚合把一個(gè)LinkedList作為屬性也能達(dá)到同樣的目的,例子中我是用
繼承,從構(gòu)造方法中傳入一個(gè)可以放置的最大值。
class Table extends java.util.LinkedList{ int maxSize; public Table(int maxSize){ this.maxSize = maxSize; } }
現(xiàn)在我們要為它加兩個(gè)方法,一是廚師往上面放菜的方法,一是食客從桌子上拿菜的方法。
放菜:因?yàn)橐粡堊雷佑啥鄠€(gè)廚師放菜,所以廚師放菜的要被同步,如果桌子上已經(jīng)有十盤菜了。所有廚師就要等待:
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; }
廚師類:
由于多個(gè)廚師要往一張桌子上放菜,所以他們要操作的桌子應(yīng)該是同一個(gè)對(duì)象,我們從構(gòu)造方法中將桌子對(duì)象傳進(jìn)去以便控制在主線程中只產(chǎn)生一張桌子。
廚師做菜要用一定的時(shí)候,我用在make方法中用sleep表示他要消耗和時(shí)候,用200加上200的隨機(jī)數(shù)保證時(shí)間有200-400ms中。做好后就要往桌子上放。
這里有一個(gè)非常重要的問題一定要注意,就是對(duì)什么范圍同步的問題,因?yàn)楫a(chǎn)生競(jìng)爭(zhēng)的是桌子,所以所有putFood是同步的,而我們不能把廚師自己做菜的時(shí)間也放在同步中,因?yàn)樽霾耸歉髯宰龅摹M瑯邮晨统圆说臅r(shí)候也不應(yīng)該同步,只有從桌子中取菜的時(shí)候是競(jìng)爭(zhēng)的,而具體吃的時(shí)候是各自在吃。所以廚師類的代碼如下:
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(); } }
同理我們產(chǎn)生食客類的代碼如下:
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(); } }
這一個(gè)例子中,我們主要關(guān)注以下幾個(gè)方面:
1.同步方法要保護(hù)的對(duì)象,本例中是保護(hù)桌子,不能同時(shí)往上放菜或同時(shí)取菜。
假如我們把putFood方法和getFood方法在廚師類和食客類中實(shí)現(xiàn),那么我們應(yīng)該如此:
(以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){//方法本身不能同步,因?yàn)樗降氖莟his.即Chef的實(shí)例 synchronized (t) {//要保護(hù)的是t while (t.size() >= t.maxSize) { try { t.wait(); } catch (Exception e) {} } t.add(f); t.notifyAll(); } } }
2.同步的范圍,在本例中是放和取兩個(gè)方法,不能把做菜和吃菜這種各自不相干的工作放在受保護(hù)的范圍中。
3.參與者與容積比
對(duì)于生產(chǎn)者和消費(fèi)者的比例,以及桌子所能放置最多菜的數(shù)量三者之間的關(guān)系是影響性能的重要因素,如果是過多的生產(chǎn)者在等待,則要增加消費(fèi)者或減少生產(chǎn)者的數(shù)據(jù),反之則增加生產(chǎn)者或減少消費(fèi)者的數(shù)量。
另外如果桌子有足夠的容量可以很大程序提升性能,這種情況下可以同時(shí)提高生產(chǎn)者和消費(fèi)者的數(shù)量,但足夠大的容時(shí)往往你要有足夠大的物理內(nèi)存。
=========================================================================
多線程編程——實(shí)戰(zhàn)篇(二)
時(shí)間:2006-11-21
作者:axman
本節(jié)繼續(xù)上一節(jié)的討論。
[一個(gè)線程在進(jìn)入對(duì)象的休息室(調(diào)用該對(duì)象的wait()方法)后會(huì)釋放對(duì)該對(duì)象的鎖],基于這個(gè)原因。在同步中,除非必要,否則你不應(yīng)用使用Thread.sleep(long l)方法,因?yàn)閟leep方法并不釋放對(duì)象的鎖。
這是一個(gè)極其惡劣的品德,你自己什么事也不干,進(jìn)入sleep狀態(tài),卻抓住競(jìng)爭(zhēng)對(duì)象的監(jiān)視鎖不讓其它需要該對(duì)象監(jiān)視鎖的線程運(yùn)行,簡(jiǎn)單說是極端自私的一種行為。但我看到過很多程序員仍然有在同步方法中調(diào)用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的實(shí)例運(yùn)行后,當(dāng)前線程抓住了st實(shí)例的鎖,然后進(jìn)入了sleep。直到它睡滿60秒后才運(yùn)行到System.out.println("111");然后run方法運(yùn)行完成釋放了對(duì)st的監(jiān)視鎖,線程T2的實(shí)例才得到運(yùn)行的機(jī)會(huì)。
而如果我們把wantSleep方法改成:
public synchronized void wantSleep(){ try{ //Thread.sleep(1000*60); this.wait(1000*60); }catch(Exception e){} System.out.println("111"); }
我們看到,T2的實(shí)例所在的線程立即就得到了運(yùn)行機(jī)會(huì),首先打印了123,而T1的實(shí)例所在的線程仍然等待,直到等待60秒后運(yùn)行到System.out.println("111");方法。
所以,調(diào)用wait(long l)方法不僅達(dá)到了阻塞當(dāng)前線程規(guī)定時(shí)間內(nèi)不運(yùn)行,而且讓其它有競(jìng)爭(zhēng)需求的線程有了運(yùn)行機(jī)會(huì),這種利人不損己的方法,何樂而不為?這也是一個(gè)有良心的程序員應(yīng)該遵循的原則。
當(dāng)一個(gè)線程調(diào)用wait(long l)方法后,線程如果繼續(xù)運(yùn)行,你無法知道它是等待時(shí)間完成了還是在wait時(shí)被其它線程喚醒了,如果你非常在意它一定要等待足夠的時(shí)間才執(zhí)行某任務(wù),而不希望是中途被喚醒,這里有一個(gè)不是非常準(zhǔn)確的方法:
long l = System.System.currentTimeMillis(); wait(1000);//準(zhǔn)備讓當(dāng)前線程等待1秒 while((System.System.currentTimeMillis() - l) < 1000)//執(zhí)行到這里說明它還沒有等待到1秒 //是讓其它線程給鬧醒了 wait(1000-(System.System.currentTimeMillis()-l));//繼續(xù)等待余下的時(shí)間.
這種方法不是很準(zhǔn)確,但基本上能達(dá)到目的。
所以在同步方法中,除非你明確知道自己在干什么,非要這么做的話,你沒有理由使用sleep,wait方法足夠達(dá)到你想要的目的。而如果你是一個(gè)很保守的人,看到上面這段話后,你對(duì)sleep方法深惡痛絕,堅(jiān)決不用sleep了,那么在非同步的方法中(沒有和其它線程競(jìng)爭(zhēng)的對(duì)象),你想讓當(dāng)前線程阻塞一定時(shí)間后再運(yùn)行,應(yīng)該如何做呢?(這完全是一種賣弄,在非同步的方法中你就應(yīng)該合理地應(yīng)用sleep嘛,但如果你堅(jiān)決不用sleep,那就這樣來做吧)
public static mySleep(long l){ Object o = new Object(); synchronized(o){ try{ o.wait(l); }catch(Exception e){} } }
放心吧,沒有人能在這個(gè)方法外調(diào)用o.notify[All],所以o.wait(l)會(huì)一直等到設(shè)定的時(shí)間才會(huì)運(yùn)行完成。
[虛擬鎖的使用]
虛擬鎖簡(jiǎn)單說就是不要調(diào)用synchronized方法(它等同于synchronized(this))和不要調(diào)用synchronized(this),這樣所有調(diào)用在這個(gè)實(shí)例上的所有同步方法的線程只能有一個(gè)線程可以運(yùn)行。也就是說:
如果一個(gè)類有兩個(gè)同步方法 m1,m2,那么不僅是兩個(gè)以上線調(diào)用m1方法的線程只有一個(gè)能運(yùn)行,就是兩個(gè)分別調(diào)用m1,m2的線程也只有一個(gè)能運(yùn)行。當(dāng)然非同步方法不存在任何競(jìng)爭(zhēng),在一個(gè)線程獲取該對(duì)象的監(jiān)視鎖后這個(gè)對(duì)象的非同步方法可以被任何線程調(diào)用。
而大多數(shù)時(shí)候,我們可能會(huì)出現(xiàn)這種情況,多個(gè)線程調(diào)用m1時(shí)需要保護(hù)一種資源,而多個(gè)線程調(diào)用M2時(shí)要保護(hù)的是另一種資源,如果我們把m1,m2都設(shè)成同步方法。兩個(gè)分別調(diào)用這兩個(gè)方法的線程其實(shí)并不產(chǎn)生沖突,但它們都要獲取這個(gè)實(shí)例的鎖(同步方法是同步this)而產(chǎn)生了不必要競(jìng)爭(zhēng)。
所以這里應(yīng)該采用虛擬鎖。
即將m1和m2方法中各自保護(hù)的對(duì)象作為屬性a1,a2傳進(jìn)來,然后將同步方法改為方法的同步塊分別以a1,a2為參數(shù),這樣到少是不同線程調(diào)用這兩個(gè)不同方法時(shí)不會(huì)產(chǎn)生競(jìng)爭(zhēng),當(dāng)然如果m1,m2方法都操作同一受保護(hù)對(duì)象則兩個(gè)方法還是應(yīng)該作為同步方法。這也是應(yīng)該將方法同步還是采用同步塊的理由之一。
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(); } }
這個(gè)例子可以看到兩個(gè)線程分別調(diào)用st實(shí)例的m1和m2方法卻因?yàn)槎家@取st的監(jiān)視鎖而產(chǎn)生了競(jìng)爭(zhēng)。T2實(shí)例要在T1運(yùn)行完成后才能運(yùn)行(間隔了10秒)。而假設(shè)m1方法要操作操作一個(gè)文件 f1,m2方法要操作一個(gè)文件f2,當(dāng)然我們可以在方法中分別同步f1,f2,但現(xiàn)在還不知道f2,f2是否存在,如果不存在我們就同步了一個(gè)null對(duì)象,那么我們可以使用虛擬鎖:
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(); } }
我們看到兩個(gè)分別調(diào)用m1和m2的線程由于它們獲取不同對(duì)象的監(jiān)視鎖,它們沒有任何競(jìng)爭(zhēng)就正常運(yùn)行,只有這兩個(gè)線程同時(shí)調(diào)用m1或m2才會(huì)產(chǎn)生阻塞。
=========================================================================
多線程編程——實(shí)戰(zhàn)篇(三)
時(shí)間:2006-12-28
作者:axman
[深入了解線程對(duì)象與線程,線程與運(yùn)行環(huán)境]
在基礎(chǔ)篇中的第一節(jié),我就強(qiáng)調(diào)過,要了解多線程編程,首要的兩個(gè)概念就是線程對(duì)象和線程。現(xiàn)在我們來深入理解線程對(duì)象,線程,運(yùn)行環(huán)境之間的關(guān)系,弄清Runnable與Thread的作用。
在JAVA平臺(tái)中,序列化機(jī)制是一個(gè)非常重要的機(jī)制,如果不能理解并熟練應(yīng)用序列化機(jī)制,你就不能稱得上一個(gè)java程序員。
在JAVA平臺(tái)中,為什么有些對(duì)象中可序列化的,而有些對(duì)象就不能序列化?
能序列化的對(duì)象,簡(jiǎn)單說是一種可以復(fù)制(意味著可以按一定機(jī)制進(jìn)行重構(gòu)它)的對(duì)象,這種對(duì)象說到底就是內(nèi)存中一些數(shù)據(jù)的組合。只要按一定位置和順序組合就能完整反映這個(gè)對(duì)象。
而有些對(duì)象,是和當(dāng)前環(huán)境相關(guān)的,它反映了當(dāng)前運(yùn)行的環(huán)境和時(shí)序,所以不能被序列,否則在另外的環(huán)境和時(shí)序中就無法“還原”。
比如,一個(gè)Socket對(duì)象:
Socket sc = new Socket("111.111.111.111",80);
這個(gè)sc對(duì)象表示當(dāng)前正在運(yùn)行這段代碼的主機(jī)和IP為"111.111.111.111"的80端口之間建立的一個(gè)物理連結(jié),如果它被序列化,那么在另一個(gè)時(shí)刻在另一個(gè)主機(jī)上它如何能被還原?Socket連結(jié)一旦斷開,就已經(jīng)不存在,它不可能在另一個(gè)時(shí)間被另一個(gè)主機(jī)所重現(xiàn)。重現(xiàn)的已經(jīng)不是原來那個(gè)sc對(duì)象了。
線程對(duì)象也是這種不可序列化對(duì)象,當(dāng)我們new Thread時(shí),已經(jīng)初始化了當(dāng)前這個(gè)線程對(duì)象所在有主機(jī)的運(yùn)行環(huán)境相關(guān)的信息,線程調(diào)度機(jī)制,安全機(jī)制等只特定于當(dāng)前運(yùn)行環(huán)境的信息,假如它被序列化,在另一個(gè)環(huán)境中運(yùn)行的時(shí)候原來初始化的運(yùn)行環(huán)境的信息就不可能在新的環(huán)境中運(yùn)行。而假如要重新初始化,那它已經(jīng)不是原來那個(gè)線程對(duì)象了。
正如Socket封裝了兩個(gè)主機(jī)之間的連結(jié),但它們并不是已經(jīng)連結(jié)關(guān)傳送數(shù)據(jù)了。要想傳送數(shù)據(jù),你還要getInputStream和getOutputStream,并read和write,兩臺(tái)主機(jī)之間才開始真正的“數(shù)據(jù)連結(jié)”。
一個(gè)Thread對(duì)象并建立后,只是有了可以"運(yùn)行"的令牌,僅僅只是一個(gè)"線程對(duì)象"。只有當(dāng)它調(diào)用start()后,當(dāng)前環(huán)境才會(huì)分配給它一個(gè)運(yùn)行的"空間",讓這段代碼開始運(yùn)行。這個(gè)運(yùn)行的"空間",才叫真正的"線程"。也就是說,真正的線程是指當(dāng)前正在執(zhí)行的那一個(gè)"事件"。是那個(gè)線程對(duì)象所在的運(yùn)行環(huán)境。
明白了上面的概念,我們?cè)賮砜纯碕AVA中為什么要有Runnable對(duì)象和Thread對(duì)象。
一、從設(shè)計(jì)技巧上說,JAVA中為了實(shí)現(xiàn)回調(diào),無法調(diào)用方法指針,那么利用接口來約束實(shí)現(xiàn)者強(qiáng)制提供匹配的方法,并將實(shí)現(xiàn)該接口的類的實(shí)例作為參數(shù)來提供給調(diào)用者,這是JAVA平臺(tái)實(shí)現(xiàn)回調(diào)的重要手段。
二、但是從實(shí)際的操作來看,對(duì)于算法和數(shù)據(jù),是不依賴于任何環(huán)境的。所以把想要實(shí)現(xiàn)的操作中的算法和數(shù)據(jù)封裝到一個(gè)run方法中(由于算法本身是數(shù)據(jù)的一個(gè)部分,所以我把它們合并稱為數(shù)據(jù)),可以將離數(shù)據(jù)和環(huán)境的邏輯分離開來。使程序員只關(guān)心如何實(shí)現(xiàn)我想做的操作,而不要關(guān)心它所在的環(huán)境。當(dāng)真正的需要運(yùn)行的時(shí)候再將這段"操作"傳給一個(gè)具體當(dāng)前環(huán)境的Thread對(duì)象。
三、這是最最重要的原因:實(shí)現(xiàn)數(shù)據(jù)共享
因?yàn)橐粋€(gè)線程對(duì)象不對(duì)多次運(yùn)行。所以把數(shù)據(jù)放在Thread對(duì)象中,不會(huì)被多個(gè)線程同時(shí)訪問。簡(jiǎn)單說:
class T extends Thread{ Object x; public void run(){//......;} } T t = new T();
當(dāng)T的實(shí)例t運(yùn)行后,t所包含的數(shù)據(jù)x只能被一個(gè)t.start();對(duì)象共享,除非聲明成 static Object x;
一個(gè)t的實(shí)例數(shù)據(jù)只能被一個(gè)線程訪問。意思是"一個(gè)數(shù)據(jù)實(shí)例對(duì)應(yīng)一個(gè)線程"。
而假如我們從外部傳入數(shù)據(jù),比如
class T extends Thread{ private Object x; public T(Object x){ this.x = x; } public void run(){//......;} }
這樣我們就可以先生成一個(gè)x對(duì)象傳給多個(gè)Thread對(duì)象,多個(gè)線程共同操作一個(gè)數(shù)據(jù)。也就是"一個(gè)數(shù)據(jù)實(shí)例對(duì)應(yīng)多個(gè)線程"。
現(xiàn)在我們把數(shù)據(jù)更好地組織一下,把要操作的數(shù)據(jù)Object x和要進(jìn)行的操作一個(gè)封裝到Runnable的run()方法中,把Runnable實(shí)例從外部傳給多個(gè)Thread對(duì)象。這樣,我們就有了:
[一個(gè)對(duì)象的多個(gè)線程]
這是以后我們要介紹的線程池的重要概念。
========================================================================
多線程編程——實(shí)戰(zhàn)篇(四)
時(shí)間:2007-02-08
作者:axman
不客氣地說,至少有一半人認(rèn)為,線程的“中斷”就是讓線程停止。如果你也這么認(rèn)為,那你對(duì)多線程編程還沒有入門。
在java中,線程的中斷(interrupt)只是改變了線程的中斷狀態(tài),至于這個(gè)中斷狀態(tài)改變后帶來的結(jié)果,那是無法確定的,有時(shí)它更是讓停止中的線程繼續(xù)執(zhí)行的唯一手段。不但不是讓線程停止運(yùn)行,反而是繼續(xù)執(zhí)行線程的手段。
對(duì)于執(zhí)行一般邏輯的線程,如果調(diào)用它的interrupt()方法,那么對(duì)這個(gè)線程沒有任何影響,比如線程a正在執(zhí)行:while(條件) x ++;這樣的語句,如果其它線程調(diào)用a.interrupt();那么并不會(huì)影響a對(duì)象上運(yùn)行的線程,如果在其它線程里測(cè)試a的中斷狀態(tài)它已經(jīng)改變,但并不會(huì)停止這個(gè)線程的運(yùn)行。在一個(gè)線程對(duì)象上調(diào)用interrupt()方法,真正有影響的是wait,join,sleep方法,當(dāng)然這三個(gè)方法包括它們的重載方法。
請(qǐng)注意:[上面這三個(gè)方法都會(huì)拋出InterruptedException],記住這句話,下面我會(huì)重復(fù)。一個(gè)線程在調(diào)用interrupt()后,自己不會(huì)拋出InterruptedException異常,所以你看到interrupt()并沒有拋出這個(gè)異常,所以我上面說如果線程a正在執(zhí)行while(條件) x ++;你調(diào)用a.interrupt();后線程會(huì)繼續(xù)正常地執(zhí)行下去。
但是,如果一個(gè)線程被調(diào)用了interrupt()后,它的狀態(tài)是已中斷的。這個(gè)狀態(tài)對(duì)于正在執(zhí)行wait,join,sleep的線程,卻改變了線程的運(yùn)行結(jié)果。
一、對(duì)于wait中等待notify/notifyAll喚醒的線程,其實(shí)這個(gè)線程已經(jīng)“暫停”執(zhí)行,因?yàn)樗谀骋粚?duì)象的休息室中,這時(shí)如果它的中斷狀態(tài)被改變,那么它就會(huì)拋出異常。這個(gè)InterruptedException異常不是線程拋出的,而是wait方法,也就是對(duì)象的wait方法內(nèi)部會(huì)不斷檢查在此對(duì)象上休息的線程的狀態(tài),如果發(fā)現(xiàn)哪個(gè)線程的狀態(tài)被置為已中斷,則會(huì)拋出InterruptedException,意思就是這個(gè)線程不能再等待了,其意義就等同于喚醒它了。
這里唯一的區(qū)別是,被notify/All喚醒的線程會(huì)繼續(xù)執(zhí)行wait下面的語句,而在wait中被中斷的線程則將控制權(quán)交給了catch語句。一些正常的邏輯要被放到catch中來運(yùn)行。但有時(shí)這是唯一手段,比如一個(gè)線程a在某一對(duì)象b的wait中等待喚醒,其它線程必須獲取到對(duì)象b的監(jiān)視鎖才能調(diào)用b.notify()[All],否則你就無法喚醒線程a,但在任何線程中可以無條件地調(diào)用a.interrupt();來達(dá)到這個(gè)目的。只是喚醒后的邏輯你要放在catch中,當(dāng)然同notify/All一樣,繼續(xù)執(zhí)行a線程的條件還是要等拿到b對(duì)象的監(jiān)視鎖。
二、對(duì)于sleep中的線程,如果你調(diào)用了Thread.sleep(一年);現(xiàn)在你后悔了,想讓它早些醒過來,調(diào)用interrupt()方法就是唯一手段,只有改變它的中斷狀態(tài),讓它從sleep中將控制權(quán)轉(zhuǎn)到處理異常的catch語句中,然后再由catch中的處理轉(zhuǎn)換到正常的邏輯。同樣地,于join中的線程你也可以這樣處理。
對(duì)于一般介紹多線程模式的書上,他們會(huì)這樣來介紹:當(dāng)一個(gè)線程被中斷后,在進(jìn)入wait,sleep,join方法時(shí)會(huì)拋出異常。是的,這一點(diǎn)也沒有錯(cuò),但是這有什么意義呢?如果你知道那個(gè)線程的狀態(tài)已經(jīng)處于中斷狀態(tài),為什么還要讓它進(jìn)入這三個(gè)方法呢?當(dāng)然有時(shí)是必須這么做的,但大多數(shù)時(shí)候沒有這么做的理由,所以我上面主要介紹了在已經(jīng)調(diào)用這三個(gè)方法的線程上調(diào)用interrupt()方法讓它從"暫停"狀態(tài)中恢復(fù)過來。這個(gè)恢復(fù)過來就可以包含兩個(gè)目的:
一、[可以使線程繼續(xù)執(zhí)行],那就是在catch語句中招待醒來后的邏輯,或由catch語句轉(zhuǎn)回正常的邏輯。總之它是從wait,sleep,join的暫停狀態(tài)活過來了。
二、[可以直接停止線程的運(yùn)行],當(dāng)然在catch中什么也不處理,或return,那么就完成了當(dāng)前線程的使命,可以使在上面“暫停”的狀態(tài)中立即真正的“停止”。
中斷線程
有了上一節(jié)[線程的中斷],我們就好進(jìn)行如何[中斷線程]了。這絕對(duì)不是玩一個(gè)文字游戲。是因?yàn)?#8220;線程的中斷”并不能保證“中斷線程”,所以我要特別地分為兩節(jié)來說明。這里說的“中斷線程”意思是“停止線程”,而為什么不用“停止線程”這個(gè)說法呢?因?yàn)榫€程有一個(gè)明確的stop方法,但它是反對(duì)使用的,所以請(qǐng)大家記住,在java中以后不要提停止線程這個(gè)說法,忘記它!但是,作為介紹線程知識(shí)的我,我仍然要告訴你為什么不用“停止線程”的理由。
[停止線程]
當(dāng)在一個(gè)線程對(duì)象上調(diào)用stop()方法時(shí),這個(gè)線程對(duì)象所運(yùn)行的線程就會(huì)立即停止,并拋出特殊的ThreadDeath()異常。這里的“立即”因?yàn)樘?#8220;立即”了,就象一個(gè)正在擺弄自己的玩具的孩子,聽到大人說快去睡覺去,就放著滿地的玩具立即睡覺去了。這樣的孩子是不乖的。
假如一個(gè)線程正在執(zhí)行:
synchronized void { x = 3; y = 4; }
由于方法是同步的,多個(gè)線程訪問時(shí)總能保證x,y被同時(shí)賦值,而如果一個(gè)線程正在執(zhí)行到x = 3;時(shí),被調(diào)用了 stop()方法,即使在同步塊中,它也干脆地stop了,這樣就產(chǎn)生了不完整的殘廢數(shù)據(jù)。而多線程編程中最最基礎(chǔ)的條件要保證數(shù)據(jù)的完整性,所以請(qǐng)忘記線程的stop方法,以后我們?cè)僖膊灰f“停止線程”了。
如何才能“結(jié)束”一個(gè)線程?
[中斷線程]
結(jié)束一個(gè)線程,我們要分析線程的運(yùn)行情況。也就是線程正在干什么。如果那個(gè)孩子什么事也沒干,那就讓他立即去睡覺。而如果那個(gè)孩子正在擺弄他的玩具,我們就要讓它把玩具收拾好再睡覺。
所以一個(gè)線程從運(yùn)行到真正的結(jié)束,應(yīng)該有三個(gè)階段:
- 正常運(yùn)行.
- 處理結(jié)束前的工作,也就是準(zhǔn)備結(jié)束.
- 結(jié)束退出.
在我的JDBC專欄中我N次提醒在一個(gè)SQL邏輯結(jié)束后,無論如何要保證關(guān)閉Connnection那就是在finally從句中進(jìn)行。同樣,線程在結(jié)束前的工作應(yīng)該在finally中來保證線程退出前一定執(zhí)行:
try{ 正在邏輯 }catch(){} finally{ 清理工作 }
那么如何讓一個(gè)線程結(jié)束呢?既然不能調(diào)用stop,可用的只的interrupt()方法。但interrupt()方法只是改變了線程的運(yùn)行狀態(tài),如何讓它退出運(yùn)行?對(duì)于一般邏輯,只要線程狀態(tài)已經(jīng)中斷,我們就可以讓它退出,所以這樣的語句可以保證線程在中斷后就能結(jié)束運(yùn)行:
while(!isInterrupted()){ 正常邏輯 }
這樣如果這個(gè)線程被調(diào)用interrupt()方法,isInterrupted()為true,就會(huì)退出運(yùn)行。但是如果線程正在執(zhí)行wait,sleep,join方法,你調(diào)用interrupt()方法,這個(gè)邏輯就不完全了。
如果一個(gè)有經(jīng)驗(yàn)的程序員來處理線程的運(yùn)行的結(jié)束:
public void run(){ try{ while(!isInterrupted()){ 正常工作 } } catch(Exception e){ return; } finally{ 清理工作 } }
我們看到,如果線程執(zhí)行一般邏輯在調(diào)用innterrupt后,isInterrupted()為true,退出循環(huán)后執(zhí)行清理工作后結(jié)束,即使線程正在wait,sleep,join,也會(huì)拋出異常執(zhí)行清理工作后退出。
這看起來非常好,線程完全按最我們?cè)O(shè)定的思路在工作。但是,并不是每個(gè)程序員都有這種認(rèn)識(shí),如果他聰明的自己處理異常會(huì)如何?事實(shí)上很多或大多數(shù)程序員會(huì)這樣處理:
public void run(){ while(!isInterrupted()){ try{ 正常工作 }catch(Exception e){ //nothing } finally{ } } } }
想一想,如果一個(gè)正在sleep的線程,在調(diào)用interrupt后,會(huì)如何?wait方法檢查到isInterrupted()為true,拋出異常,而你又沒有處理。而一個(gè)拋出了InterruptedException的線程的狀態(tài)馬上就會(huì)被置為非中斷狀態(tài),如果catch語句沒有處理異常,則下一次循環(huán)中isInterrupted()為false,線程會(huì)繼續(xù)執(zhí)行,可能你N次拋出異常,也無法讓線程停止。
那么如何能確保線程真正停止?在線程同步的時(shí)候我們有一個(gè)叫“二次惰性檢測(cè)”(double check),能在提高效率的基礎(chǔ)上又確保線程真正中同步控制中。那么我把線程正確退出的方法稱為“雙重安全退出”,即不以isInterrupted()為循環(huán)條件。而以一個(gè)標(biāo)記作為循環(huán)條件:
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{ } } } }
試試這段程序,可以正確工作嗎?
對(duì)于這段程序仍然還有很多可說的地方,先到這里吧。
=======================================================================
http://dev2dev.bea.com.cn/bbsdoc/20070208338913.html