|
線(xiàn)程是Java的一大特色,從語(yǔ)言上直接支持線(xiàn)程,線(xiàn)程對(duì)于進(jìn)程來(lái)講的優(yōu)勢(shì)在于創(chuàng)建的代價(jià)很小,上下文切換迅速,當(dāng)然其他的優(yōu)勢(shì)還有很多,缺點(diǎn)也是有的,比如說(shuō)對(duì)于開(kāi)發(fā)人員來(lái)講要求比較高,不容易操作,但是Java的線(xiàn)程的操作已經(jīng)簡(jiǎn)化了很多,是一個(gè)比較成熟的模型。很多時(shí)候,我們都用不到線(xiàn)程,但是當(dāng)我們有一天不走運(yùn)(或者走運(yùn))的時(shí)候,我們必須要面對(duì)這個(gè)問(wèn)題的時(shí)候,應(yīng)該怎么辦呢?本文是我的學(xué)習(xí)筆記和一些總結(jié),試圖解決這個(gè)問(wèn)題,引領(lǐng)還沒(méi)有接觸過(guò)Java 線(xiàn)程的開(kāi)發(fā)人員進(jìn)入一個(gè)Java線(xiàn)程的世界,其實(shí)很多東西在網(wǎng)路上已經(jīng)有朋友總結(jié)過(guò)了,不過(guò)我感覺(jué)沒(méi)有比較循序漸進(jìn),要么太基礎(chǔ),要么太高深,所以這邊我由淺到深的總結(jié)一下。但是很顯然,我的資歷尚淺,能力也很有限,如果有什么錯(cuò)誤還望不吝賜教!麻煩發(fā)送mail到:fantian830211@163.com 而且,這些大部份的都有源碼,如果需要也可以發(fā)mail到這個(gè)郵箱,真的非常希望有人能指正我的錯(cuò)誤! (一) 基本的API介紹 1. 如何創(chuàng)建一個(gè)可以執(zhí)行的線(xiàn)程類(lèi) 創(chuàng)建一個(gè)線(xiàn)程有兩個(gè)辦法:繼承Thread類(lèi)或者實(shí)現(xiàn)Runnable接口。 首先:繼承Thread類(lèi) 這里一般只需要我們來(lái)重寫(xiě)run這個(gè)方法。下面是代碼: public class SimpleThread extends Thread { public SimpleThread() { start(); } @Override public void run() { while (true) { System.out.println(this); // Imply other thread can run now, but we cannot assume that it will // work well every time, actually , most of time we can get the same // result, but not to a certainty. // yield(); try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } 其次:實(shí)現(xiàn)Runnable接口,代碼如下: Public class Inner implements Runnable { private Thread thread; public Inner(String name) { thread = new Thread(this, name); thread.start(); } public void run() { while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } } } 2. 幾個(gè)常用的API 這邊介紹幾個(gè)常見(jiàn)而且重要的的線(xiàn)程API,這邊JDK文檔有更加詳細(xì)的說(shuō)明,其實(shí)JDK的文檔就是個(gè)很好的學(xué)習(xí)資料,常備很重要哦!
這邊只是介紹了幾個(gè)常用的API,但是非常重要,其他的API可以查看JDK的相關(guān)文檔。但是在操作系統(tǒng)的概念中,很顯然,對(duì)于一個(gè)線(xiàn)程應(yīng)該還有別的狀態(tài),對(duì),確實(shí)還有,但是Java在實(shí)現(xiàn)的映射的時(shí)候,也實(shí)現(xiàn)了這些方法,只是不贊成使用,下面的主題將討論這些方法以及這些方法的替代方法。 3. 已經(jīng)不贊成使用的方法 對(duì)于一些不應(yīng)該再使用的東西,有時(shí)候被稱(chēng)為反模式antipattern。這些都是概念上的東西,對(duì)于我們開(kāi)發(fā)人員來(lái)講,需要做的就是寫(xiě)出好的代碼。
4. 跟線(xiàn)程相關(guān)的關(guān)鍵字 跟線(xiàn)程相關(guān)的關(guān)鍵字我能夠想到的就下面兩個(gè):
其實(shí)線(xiàn)程的使用不在于語(yǔ)言的API,而在于對(duì)操作系統(tǒng)的理解和一些常見(jiàn)的調(diào)度算法,其實(shí)個(gè)人理解經(jīng)驗(yàn)比較重要,后邊介紹到線(xiàn)程的實(shí)現(xiàn)模式和設(shè)計(jì)模式。其實(shí)我還是以前的想法:對(duì)于語(yǔ)言的學(xué)習(xí),首先學(xué)習(xí)語(yǔ)法和API,然后學(xué)習(xí)如何使用這些API在語(yǔ)法的框架內(nèi)編寫(xiě)出高效的程序。很顯然,模式就是實(shí)現(xiàn)后邊的重要方法。模式常見(jiàn)的分類(lèi)有實(shí)現(xiàn)模式、設(shè)計(jì)模式和架構(gòu)模式。這里限于本人的能力問(wèn)題,沒(méi)有理解到架構(gòu)上面去,所以這里只是研究了前兩個(gè)。 (二) 線(xiàn)程實(shí)現(xiàn)模式 實(shí)現(xiàn)模式這邊主要參考自Effective Java這本書(shū),至少分類(lèi)是,但是很多內(nèi)容應(yīng)該會(huì)很不相同,當(dāng)然還有Think in java。Effective Java是短小精悍的一本書(shū),其中有太多的Java的關(guān)于實(shí)現(xiàn)模式的建議,但是這邊把這本書(shū)的內(nèi)容歸類(lèi)到實(shí)現(xiàn)模式,是我個(gè)人的想法,如果有什么不正確,萬(wàn)望指正。但是,個(gè)人認(rèn)為這些概念性的東西仍然不會(huì)損害到我們需要討論的問(wèn)題的實(shí)質(zhì)。 1. 共享數(shù)據(jù)同步 上面有提到過(guò)synchronized關(guān)鍵字,這個(gè)關(guān)鍵字保證一段代碼同時(shí)只能有一個(gè)線(xiàn)程在執(zhí)行,保證別人不會(huì)看到對(duì)象處于不一致的狀態(tài)中。對(duì)象將從一種一致的狀態(tài)轉(zhuǎn)變到另一種一致的狀態(tài),后來(lái)的線(xiàn)程將會(huì)看到后一種狀態(tài)。 在Java中,虛擬機(jī)保證原語(yǔ)類(lèi)型(除double和long)的讀寫(xiě)都是原子性的。即不需要同步,但是如果不對(duì)這樣的數(shù)據(jù)讀寫(xiě)進(jìn)行同步,那么后果將很?chē)?yán)重??梢詤⒄?/span>Effective Java的解釋?zhuān)@里還要簡(jiǎn)單的提示意下,Effective Java中有提到double check這種方式,而且我的源代碼中多次用到這種方法,單是需要提醒一下,如果用這種方式來(lái)實(shí)現(xiàn)singleton的話(huà),就不可以了,因?yàn)檫@樣有可能導(dǎo)致不完整的對(duì)象被使用,單是源碼中的double check用的都是原語(yǔ)類(lèi)型,所以OK。 這邊的建議是如果修改原語(yǔ)類(lèi)型或者非可變類(lèi)的屬性,可以同步或者使用volatile關(guān)鍵字。如果是其他對(duì)象,必須同步。關(guān)于盡量少使用同步,這邊的建議是,我們這樣的初學(xué)者在不知道如何優(yōu)化的情況下就不要優(yōu)化,我們要的是正確的程序,而不是快的程序。 2. wait方法的使用 wait方法是一個(gè)很重要的方法,前面有介紹過(guò)這個(gè)方法,不但可以使一個(gè)線(xiàn)程等待,而且可以作為實(shí)現(xiàn)suspend的替代方法的一個(gè)方法。 Wait方法的標(biāo)準(zhǔn)使用方式如下: synchronized (obj) { while (condition) wait(); } 這里,對(duì)應(yīng)wait方法還有一個(gè)notify和notifyAll方法,到底我們應(yīng)該如何使用這兩個(gè)方法來(lái)喚醒等待的線(xiàn)程呢?很顯然notifyAll的使用是最安全的,但是會(huì)帶來(lái)性能的降低。這里又提到我們初學(xué)者,應(yīng)該優(yōu)先考慮這個(gè)方法,而不是notify。 3. 不要依賴(lài)線(xiàn)程調(diào)度器,管好自己的事情 Thread.yield這個(gè)方法并不能保證線(xiàn)程的公平運(yùn)行,所以這個(gè)方法不應(yīng)該依賴(lài)。還有就是線(xiàn)程的優(yōu)先級(jí),Java的線(xiàn)程優(yōu)先級(jí)有10個(gè)等級(jí),但是這個(gè)等級(jí)幾乎沒(méi)有什么用處,所以我們也不應(yīng)該依賴(lài)這個(gè)優(yōu)先級(jí)來(lái)控制程序,當(dāng)然仍然可以?xún)?yōu)化一些服務(wù),但是不能保證這些服務(wù)一定被優(yōu)化了。我們應(yīng)該盡量控制對(duì)critical resources的方法線(xiàn)程數(shù),而不是用優(yōu)先級(jí)或者yield來(lái)實(shí)現(xiàn)對(duì)資源的訪(fǎng)問(wèn)。 4. 不要使用線(xiàn)程組 線(xiàn)程組是一個(gè)過(guò)時(shí)的API,所以不建議使用。但是也不是一無(wú)是處,“存在即合理”嘛! (三) 線(xiàn)程設(shè)計(jì)模式 什么是模式呢?Martin Flower先生這樣描述:一個(gè)模式,就是在實(shí)際的上下文中,并且在其他上下文中也會(huì)有用的想法。 這邊的線(xiàn)程設(shè)計(jì)模式大部分參考自 1. Single Threaded Execution 這個(gè)模式在Java里說(shuō)的話(huà)有點(diǎn)多余,但是這邊還是先拿這個(gè)開(kāi)胃一下。很明顯,從字面的意思,就是說(shuō)同一時(shí)刻只有一個(gè)線(xiàn)程在執(zhí)行,Java里用synchronized這個(gè)關(guān)鍵字來(lái)實(shí)現(xiàn)這個(gè)模式。確實(shí)多余 L!看看UML吧!其實(shí)用這個(gè)圖來(lái)描述有點(diǎn)不好。其實(shí)應(yīng)該用別的圖來(lái)描述會(huì)比較好!比如協(xié)作圖。 2. Guarded Suspension 網(wǎng)上有一個(gè)比較好的描述方式:要等我準(zhǔn)備好噢! 這里我們假設(shè)一種情況:一個(gè)服務(wù)器用一個(gè)緩沖區(qū)來(lái)保存來(lái)自客戶(hù)端的請(qǐng)求,服務(wù)器端從緩沖區(qū)取得請(qǐng)求,如果緩沖區(qū)沒(méi)有請(qǐng)求,服務(wù)器端線(xiàn)程等待,直到被通知有請(qǐng)求了,而客戶(hù)端負(fù)責(zé)發(fā)送請(qǐng)求。 很顯然,我們需要對(duì)緩沖區(qū)進(jìn)行保護(hù),使得同一時(shí)刻只能有一個(gè)服務(wù)器線(xiàn)程在取得request,也只能同一時(shí)刻有一個(gè)客戶(hù)端線(xiàn)程寫(xiě)入服務(wù)。 用UML描述如下:
具體實(shí)現(xiàn)可以參看代碼。 但是,這個(gè)模式有一點(diǎn)點(diǎn)瑕疵,那就是緩沖區(qū)沒(méi)有限制,對(duì)于有的情況就不會(huì)合適,比如說(shuō)您的緩沖區(qū)所能占用的空間受到限制。下面的Producer Consumer Pattern應(yīng)該會(huì)有所幫助。 3. Producer Consumer Producer Consumer跟上面的Guarded Suspension很像,唯一的區(qū)別在于緩沖區(qū),Guarded Suspension模式的緩沖區(qū)沒(méi)有限制,所以,他們適用的場(chǎng)合也就不一樣了,很顯然,這個(gè)考慮應(yīng)該基于內(nèi)存是否允許。Producer Consumer的緩沖區(qū)就像一個(gè)盒子,如果裝滿(mǎn)了,就不能再裝東西,而等待有人拿走一些,讓后才能繼續(xù)放東西,這是個(gè)形象的描述。可以參考下面的UML,然后具體可以參看源碼。
4. Worker Thread Worker Thread與上面的Producer-consumer模式的區(qū)別在于Producer-consumer只是專(zhuān)注于生產(chǎn)與消費(fèi),至于如何消費(fèi)則不管理。其實(shí)Worker Thread模式是Producer-consumer與Command模式的結(jié)合。這邊簡(jiǎn)單描述一下Command pattern。用UML就和衣很清晰的描述Command pattern。
這個(gè)模式在我們的很多MVC框架中幾乎都會(huì)用到,以后我也想寫(xiě)一個(gè)關(guān)于Web應(yīng)用的總結(jié),會(huì)提到具體的應(yīng)用。其實(shí)Command pattern模式的核心就是針對(duì)接口編程,然后存儲(chǔ)命令,根據(jù)客戶(hù)短的請(qǐng)求取得相應(yīng)的命令,然后執(zhí)行,這個(gè)跟我們的Web請(qǐng)求實(shí)在是太像了,其實(shí)Struts就是這樣做的,容器相當(dāng)于Client,然后控制器Servlet相當(dāng)于Invoker,Action相當(dāng)于ICommand,那么Receiver相當(dāng)于封裝在Action中的對(duì)象了,比如Request等等。 上面描述過(guò)Command pattern之后,我們回到Worker模式。 這邊看看worker的UML:
從圖中可以看到,CommandBuffer這個(gè)緩沖區(qū)不僅僅能夠存儲(chǔ)命令,而且可以控制消費(fèi)者WorkerThread。這就是Worker模式。下面的Sequence應(yīng)該會(huì)更加明確的描述這個(gè)模式,具體可以參看代碼。
5. Thread-Per-Message Thread-Per-Message模式是一個(gè)比較常用的模式了,如果我們有一個(gè)程序需要打開(kāi)一個(gè)很大的文件,打開(kāi)這個(gè)文件需要很長(zhǎng)的時(shí)間,那么我們就可以設(shè)計(jì)讓一個(gè)線(xiàn)程來(lái)一行一行的讀入文件,而不是一次性的全部打開(kāi),這樣從外部看起來(lái)就不會(huì)有停頓的感覺(jué)。這個(gè)模式Future模式一起學(xué)習(xí)。 6. Read-Write-Lock 考慮這樣一種情況:有一個(gè)文件,有很多線(xiàn)程對(duì)他進(jìn)行讀寫(xiě),當(dāng)有線(xiàn)程在讀的時(shí)候,不允許寫(xiě),這樣是為了保證文件的一致性。當(dāng)然可以很多線(xiàn)程一起讀,這個(gè)沒(méi)有問(wèn)題。如果有線(xiàn)程在寫(xiě),其他線(xiàn)程不允許讀寫(xiě)。如果要比較好的處理這種情況,我們可以考慮使用Read-Write-Lock模式。 這個(gè)模式可以如下描述:
其實(shí)這個(gè)模式的關(guān)鍵在于鎖實(shí)現(xiàn),這里有個(gè)簡(jiǎn)單的實(shí)現(xiàn)如下: public class Lock { private volatile int readingReaders = 0; @SuppressWarnings("unused") private volatile int writingWriters = 0; @SuppressWarnings("unused") private volatile int waitingWriters = 0; public synchronized void lockRead() { try { while (writingWriters > 0 || waitingWriters > 0) { wait(); } } catch (InterruptedException e) { // null } readingReaders++; } public synchronized void unlockRead() { readingReaders--; notifyAll(); } public synchronized void lockWrite() { waitingWriters++; try { while (writingWriters > 0 || readingReaders > 0) { wait(); } } catch (InterruptedException e) { // null } finally { waitingWriters--; } writingWriters++; } public synchronized void unlockWrite() { writingWriters--; notifyAll(); } } 其實(shí)在鎖里還可以添加優(yōu)先級(jí)之類(lèi)的控制。 7. Future Future模式是Proxy模式和Thread-Per-Message模式的結(jié)合。考慮下面的情況: 比如我們的word文檔,里頭有很多圖片在末尾,我們打開(kāi)這個(gè)文檔的時(shí)候會(huì)需要同時(shí)讀取這些圖片文件,但是很明顯,如果剛剛開(kāi)始就全部讀取進(jìn)來(lái)的話(huà)會(huì)消耗太多的內(nèi)存和時(shí)間,使得顯示出現(xiàn)停頓的現(xiàn)象。那么我們應(yīng)該怎么做呢,我們可以做這樣一個(gè)對(duì)象,這個(gè)對(duì)象代表需要讀入的圖片,把這個(gè)對(duì)象放在圖片的位置上,當(dāng)需要顯示這個(gè)圖片的時(shí)候,我們才真正的填充這個(gè)對(duì)象。這個(gè)就是Proxy模式了。當(dāng)然Proxy不僅僅是這么個(gè)意思,Proxy的真正意思是我們之需要訪(fǎng)問(wèn)Proxy來(lái)操作我們真正需要操作的對(duì)象,以便實(shí)現(xiàn)對(duì)客戶(hù)段的控制。 這邊先簡(jiǎn)單描述一下Proxy模式:
當(dāng)Client請(qǐng)求的時(shí)候,我們用Proxy代替RealObject載入,當(dāng)Client真正需要getObject的時(shí)候,Proxy將調(diào)用RealObject的RealObject方法,獲得真正的RealObjct。用Sequence來(lái)描述上面這段話(huà):
下面回到Future模式,這個(gè)模式就是我們不需要真正對(duì)象的時(shí)候,首先生成一個(gè)Proxy對(duì)象來(lái)替代,然后產(chǎn)生一個(gè)線(xiàn)程來(lái)讀取真正的對(duì)象,讀取結(jié)束之后將這個(gè)對(duì)象設(shè)置到Proxy中,當(dāng)真正需要這個(gè)對(duì)象的時(shí)候,我們可以從Proxy中取到。如下:
具體可以參看代碼的實(shí)現(xiàn)。 8. Two-phase Termination Two-phase Termination模式就是讓線(xiàn)程正常結(jié)束,也就是結(jié)束之前進(jìn)行一些善后處理,釋放掉該釋放的資源,完成自己當(dāng)前的任務(wù)。在Java語(yǔ)言中,有一個(gè)方法stop,這個(gè)方法會(huì)使當(dāng)前線(xiàn)程結(jié)束,但是我們不應(yīng)該使用這個(gè)方法,因?yàn)樗麑?huì)導(dǎo)致災(zāi)難性的后果。那么我們應(yīng)該怎么做呢?這里其實(shí)上面有實(shí)現(xiàn)過(guò),就是使用設(shè)置標(biāo)志的方法來(lái)替代stop方法。具體可以查看:已經(jīng)不贊成使用的方法和代碼。 9. Thread-Specific Storage Thread-Specific Storage模式的考慮是當(dāng)資源的訪(fǎng)問(wèn)不需要線(xiàn)程的通信的時(shí)候,我們可以使用這個(gè)模式,這個(gè)模式的做法是每個(gè)線(xiàn)程有自己的一個(gè)區(qū)域,來(lái)存儲(chǔ)自己的變量,然后需要的時(shí)候操作這個(gè)變量。在Java中,已經(jīng)實(shí)現(xiàn)了ThreadLocal,我們可以用他來(lái)實(shí)現(xiàn)這個(gè)模式,這邊有一個(gè)簡(jiǎn)單的實(shí)現(xiàn): public class MyThreadLocal { @SuppressWarnings( { "unchecked", "unused" }) private Map storage = Collections.synchronizedMap(new HashMap()); @SuppressWarnings("unchecked") public Object get() { Thread current = Thread.currentThread(); Object obj = storage.get(current); if (obj == null && !storage.containsKey(current)) { obj = initValue(); storage.put(current, obj); } return obj; } @SuppressWarnings("unchecked") public void set(Object obj) { storage.put(Thread.currentThread(), obj); } public Object initValue() { return null; } } 10. Immutable 其實(shí)多線(xiàn)程的問(wèn)題有一個(gè)很大的麻煩就是如何控制資源的同步,就是防止當(dāng)前線(xiàn)程的中間狀態(tài)被下一個(gè)線(xiàn)程看到,這個(gè)有兩個(gè)辦法可以實(shí)現(xiàn),首先,就是同時(shí)只能有一個(gè)線(xiàn)程在訪(fǎng)問(wèn),另外一個(gè)辦法就是使得資源變成非可變類(lèi),既然是不變的,大家就可以隨便訪(fǎng)問(wèn)了。 11. Balking 考慮這樣一個(gè)情況:有一個(gè)比較好的洗手的地方,你可以按按鈕來(lái)放水,其實(shí)它旁邊還有一個(gè)傳感器,可以感受到您的手過(guò)來(lái)了,應(yīng)該放水,那么如果您已經(jīng)按過(guò)按鈕,水正在放,那么傳感器的放水信號(hào)應(yīng)該如何處理呢,很顯然,需要丟棄這次放水請(qǐng)求。反過(guò)來(lái)也一樣。 Sequence如下:
線(xiàn)程的學(xué)習(xí)筆記和一些總結(jié)大概就這么多了,想想這段時(shí)間的學(xué)習(xí),花費(fèi)了很多的時(shí)間,但是效果是很多東西只是從書(shū)本上看來(lái)的,實(shí)在是可惜沒(méi)有辦法真正的實(shí)踐一下,所以這些東西其實(shí)應(yīng)該有更深刻的理解。希望有這么一天?。。?! |
|
|
來(lái)自: shaobin0604@1... > 《Java》