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