線程是Java的一大特色,從語言上直接支持線程,線程對于進程來講的優(yōu)勢在于創(chuàng)建的代價很小,上下文切換迅速,當然其他的優(yōu)勢還有很多,缺點也是有的,比如說對于開發(fā)人員來講要求比較高,不容易操作,但是Java的線程的操作已經(jīng)簡化了很多,是一個比較成熟的模型。很多時候,我們都用不到線程,但是當我們有一天不走運(或者走運)的時候,我們必須要面對這個問題的時候,應該怎么辦呢?本文是我的學習筆記和一些總結,試圖解決這個問題,引領還沒有接觸過Java 線程的開發(fā)人員進入一個Java線程的世界,其實很多東西在網(wǎng)路上已經(jīng)有朋友總結過了,不過我感覺沒有比較循序漸進,要么太基礎,要么太高深,所以這邊我由淺到深的總結一下。但是很顯然,我的資歷尚淺,能力也很有限,如果有什么錯誤還望不吝賜教!麻煩發(fā)送mail到:
fantian830211@163.com 而且,這些大部份的都有源碼,如果需要也可以發(fā)mail到這個郵箱,真的非常希望有人能指正我的錯誤!
(一) 基本的API介紹
1. 如何創(chuàng)建一個可以執(zhí)行的線程類
創(chuàng)建一個線程有兩個辦法:繼承Thread類或者實現(xiàn)Runnable接口。
首先:繼承Thread類
這里一般只需要我們來重寫run這個方法。下面是代碼:
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();
}
}
}
}
其次:實現(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. 幾個常用的API
這邊介紹幾個常見而且重要的的線程API,這邊JDK文檔有更加詳細的說明,其實JDK的文檔就是個很好的學習資料,常備很重要哦!
方法
說明
start
使線程開始執(zhí)行,實際上這個方法會調用下面的run這個方法,如果這個線程已經(jīng)開始執(zhí)行,則會扔出IllegalThreadStateException
sleep
是當前已經(jīng)運行的線程休眠一段時間。如果當前線程已經(jīng)被別的線程中斷的話,將會扔出InterruptedException,而且interrupted標志也會被清空。這個方法有兩個版本,具體參看JDK文檔。
run
線程執(zhí)行的業(yè)務邏輯應該在這里實現(xiàn)。
join
等待另外一個線程死亡。如果當前線程已經(jīng)被別的線程中斷的話,將會扔出InterruptedException,而且interrupted標志也會被清空。
yield
使當前線程臨時的中斷執(zhí)行,來允許其他線程可以執(zhí)行,因為Java的線程模型實際上映射到操作系統(tǒng)的線程模型,所以對于不同的操作系統(tǒng),這個方法的就有不同的意義。對于非搶占式Operating System,這個方法使得其他線程得到運行的機會,但是對于搶占式的OS,這個方法沒有太多的意義。關于這個方法,后邊還有更多的介紹。
wait
Wait方法和后邊的兩個方法都來自Object。看過Java源碼的可以知道,這三個方法都是Native方法,使比較直接的和操作系統(tǒng)打交道的方法。
這個方法的作用是讓當前線程等待,直到被喚醒或者等待的時間結束。當前線程進入等待隊列的時候,會放棄當前所有的資源,所以當前線程必須獲得這些對象的Monitor,否則會扔出IllegalMonitorStateException 關于wait方法的更多,后邊會有介紹到。
notify
通知其他線程可以使用資源了。這個方法的使用要求很多,總之需要當前線程獲得被調用的notify方法的對象的monitor。比如:
synchronized (person) {
person.notify();
}
其實,獲得monitor的方法還有別的,這里簡單介紹一下:
1. 執(zhí)行這個對象的一個同步的方法
2. 執(zhí)行這個對象的同步塊
3. 執(zhí)行一個同步的靜態(tài)方法
notifyAll
除了通知所有的線程可以準備執(zhí)行之外,跟上面的方法要求一樣。但是只有一個線程會被選擇然后執(zhí)行,這個就跟優(yōu)先級和其他狀態(tài)有關系了。
interrupt
中斷線程。
這邊只是介紹了幾個常用的API,但是非常重要,其他的API可以查看JDK的相關文檔。但是在操作系統(tǒng)的概念中,很顯然,對于一個線程應該還有別的狀態(tài),對,確實還有,但是Java在實現(xiàn)的映射的時候,也實現(xiàn)了這些方法,只是不贊成使用,下面的主題將討論這些方法以及這些方法的替代方法。
3. 已經(jīng)不贊成使用的方法
對于一些不應該再使用的東西,有時候被稱為反模式antipattern。這些都是概念上的東西,對于我們開發(fā)人員來講,需要做的就是寫出好的代碼。
方法
說明
stop
強制使當前的線程停止執(zhí)行。實際上,作為開發(fā)人員我們會意識到,線程的復雜程度是沒有邊際的,而這個方法這樣武斷的停止一個線程,必然導致問題產(chǎn)生。也就是說,這個方法天生就有問題。比如說一個線程掌握了很多對象,并且改變了其中一些的狀態(tài),如果突然當前對象突然被停止,將會釋放這些對象的monitor,這個時候被改變狀態(tài)的對象就是被損壞的對象,其他線程再來操作的時候問題就出來了。
替代的辦法就是讓當前線程正常結束,不使用這個方法。就是我們設置一些標志,如果這些標志滿足的時候,我們結束線程。下面用JDK的例子:
private Thread blinker;
public void start() {
blinker = new Thread(this);
blinker.start();
}
public void stop() {
blinker.stop(); // UNSAFE!
}
public void run() {
Thread thisThread = Thread.currentThread();
while (true) {
try {
thisThread.sleep(interval);
} catch (InterruptedException e){
}
//do something
}
}
修改后:
private volatile Thread blinker; public void stop() { blinker = null; } public void run() { Thread thisThread = Thread.currentThread(); //Check the flag while (blinker == thisThread) { try { thisThread.sleep(interval); } catch (InterruptedException e){ } //do something } }
當然如果這個方法中間有wait方法的調用的話,而且正在等待,我們可以使用這個辦法來結束:
Thread.currentThread().interrupt();
然后處理InterruptedException
這個我也實現(xiàn)了避免使用stop方法的一個類,在源碼中可以看到。
suspend
這個方法天生就有導致死鎖的可能。如果當前線程持有很多對象的鎖,但是當他suspend的時候,這些鎖是不會釋放的,想想就知道應該什么可能會發(fā)生,所以這個方法應該盡量不用。
這里我們有辦法替代這個方法,其實根替代stop的方法差不多,就是用wait方法來實現(xiàn)掛起,而不是事實上的掛起。比如:
@Override
@SuppressWarnings("static-access")
public void run() {
while (true) {
try {
Thread.currentThread().sleep(1000);
// Double check
if (isSuspended) {
synchronized (this) {
while (isSuspended) {
wait();
}
}
}
} catch (InterruptedException e) {
// null
}
}
}
這樣做可以同樣實現(xiàn)掛起,但是仍然會釋放資源。
resume
很顯然,這個方法和上面的方法是對應的,所以上面用了wait方法來替代,所以這邊應該用notify這個方法或者notifyAll這個方法來替代。
其實這邊可以把上面的實現(xiàn)方式結合起來,實現(xiàn)一個可以安全stop和suspend的線程。這個在我的源碼里有實現(xiàn),但是不知道是不是對的。不過感覺原則應該沒有問題,那就是設置標志來結束或者掛起線程,而不是使用這些過時的方法。
4. 跟線程相關的關鍵字
跟線程相關的關鍵字我能夠想到的就下面兩個:
關鍵字
說明
volatile
這個關鍵字告訴編譯器不要對這個屬性或者值進行優(yōu)化,也就是為了保證這個變量的同步性,如果這個值被更新,其他線程應該可以馬上訪問到最新的值,而不是“臟值”。其實這個關鍵字是同步互斥的一個模型,但是現(xiàn)在沒有實現(xiàn)。
synchronized
給對象或者Class加鎖,分為對象鎖和Class鎖。
對象鎖只是加在當前對象上,對別的對象沒有影響,這種加鎖一般都是把這個關鍵字用在方法和同步塊上面。
Class鎖就是加在這個Class上面,所有的其他對象想訪問這個Class的對象的任何同步方法,必須獲得這個鎖。這種鎖一般把這個關鍵字用在靜態(tài)方法中,或者顯示的這樣實現(xiàn):
synchronized (AClass.class) {
while (isSuspended) {
wait();
}
}
一般我們很少用Class鎖。
這里簡單提一下monitor,個人感覺這里把monitor認為是一把鎖也可以。網(wǎng)絡上有朋友解釋的比較好:在java中,每個對象只有一個相應的monitor,一個mutex,而每一個monitor都可以有多個“doors”可以進入,即,同一個monitor中被守護的代碼可以在不同的地方,因為同一個對象可以出現(xiàn)在不同的代碼段,只要mutex鎖定的對象是同一個,每個入口都用Synchronized關鍵字表明,當一個線程通過了Synchronized關鍵字,它就所住了該monitor所有的doors
其實線程的使用不在于語言的API,而在于對操作系統(tǒng)的理解和一些常見的調度算法,其實個人理解經(jīng)驗比較重要,后邊介紹到線程的實現(xiàn)模式和設計模式。其實我還是以前的想法:對于語言的學習,首先學習語法和API,然后學習如何使用這些API在語法的框架內編寫出高效的程序。很顯然,模式就是實現(xiàn)后邊的重要方法。模式常見的分類有實現(xiàn)模式、設計模式和架構模式。這里限于本人的能力問題,沒有理解到架構上面去,所以這里只是研究了前兩個。
(二) 線程實現(xiàn)模式
實現(xiàn)模式這邊主要參考自Effective Java這本書,至少分類是,但是很多內容應該會很不相同,當然還有Think in java。Effective Java是短小精悍的一本書,其中有太多的Java的關于實現(xiàn)模式的建議,但是這邊把這本書的內容歸類到實現(xiàn)模式,是我個人的想法,如果有什么不正確,萬望指正。但是,個人認為這些概念性的東西仍然不會損害到我們需要討論的問題的實質。
1. 共享數(shù)據(jù)同步
上面有提到過synchronized關鍵字,這個關鍵字保證一段代碼同時只能有一個線程在執(zhí)行,保證別人不會看到對象處于不一致的狀態(tài)中。對象將從一種一致的狀態(tài)轉變到另一種一致的狀態(tài),后來的線程將會看到后一種狀態(tài)。
在Java中,虛擬機保證原語類型(除double和long)的讀寫都是原子性的。即不需要同步,但是如果不對這樣的數(shù)據(jù)讀寫進行同步,那么后果將很嚴重。可以參照Effective Java的解釋,這里還要簡單的提示意下,Effective Java中有提到double check這種方式,而且我的源代碼中多次用到這種方法,單是需要提醒一下,如果用這種方式來實現(xiàn)singleton的話,就不可以了,因為這樣有可能導致不完整的對象被使用,單是源碼中的double check用的都是原語類型,所以OK。 這邊的建議是如果修改原語類型或者非可變類的屬性,可以同步或者使用volatile關鍵字。如果是其他對象,必須同步。關于盡量少使用同步,這邊的建議是,我們這樣的初學者在不知道如何優(yōu)化的情況下就不要優(yōu)化,我們要的是正確的程序,而不是快的程序。
2. wait方法的使用
wait方法是一個很重要的方法,前面有介紹過這個方法,不但可以使一個線程等待,而且可以作為實現(xiàn)suspend的替代方法的一個方法。
Wait方法的標準使用方式如下:
synchronized (obj) {
while (condition)
wait();
}
這里,對應wait方法還有一個notify和notifyAll方法,到底我們應該如何使用這兩個方法來喚醒等待的線程呢?很顯然notifyAll的使用是最安全的,但是會帶來性能的降低。這里又提到我們初學者,應該優(yōu)先考慮這個方法,而不是notify。
3. 不要依賴線程調度器,管好自己的事情
Thread.yield這個方法并不能保證線程的公平運行,所以這個方法不應該依賴。還有就是線程的優(yōu)先級,Java的線程優(yōu)先級有10個等級,但是這個等級幾乎沒有什么用處,所以我們也不應該依賴這個優(yōu)先級來控制程序,當然仍然可以優(yōu)化一些服務,但是不能保證這些服務一定被優(yōu)化了。我們應該盡量控制對critical resources的方法線程數(shù),而不是用優(yōu)先級或者yield來實現(xiàn)對資源的訪問。
4. 不要使用線程組
線程組是一個過時的API,所以不建議使用。但是也不是一無是處,“存在即合理”嘛!
(三) 線程設計模式
什么是模式呢?Martin Flower先生這樣描述:一個模式,就是在實際的上下文中,并且在其他上下文中也會有用的想法。
這邊的線程設計模式大部分參考自林信良先生的《設計模式》,還有一些網(wǎng)路的文章,這些都是前輩們在使用線程時候的經(jīng)驗,非常值得我們借鑒。還有就是林信良先生的設計模式非常通俗易懂,是入門級選手的最佳選擇。關于線程的模式應該還有別的,只是我這邊現(xiàn)在只能總結這么多了,能力有限。這邊用大量的UML來描述這些模式,但是由于我的UML學的不好,而且工具用的不怎么熟,畫的圖應該會有些問題,當做草圖來看就好了。
1. Single Threaded Execution
這個模式在Java里說的話有點多余,但是這邊還是先拿這個開胃一下。很明顯,從字面的意思,就是說同一時刻只有一個線程在執(zhí)行,Java里用synchronized這個關鍵字來實現(xiàn)這個模式。確實多余 L!看看UML吧!其實用這個圖來描述有點不好。其實應該用別的圖來描述會比較好!比如協(xié)作圖。
2. Guarded Suspension
網(wǎng)上有一個比較好的描述方式:要等我準備好噢!
這里我們假設一種情況:一個服務器用一個緩沖區(qū)來保存來自客戶端的請求,服務器端從緩沖區(qū)取得請求,如果緩沖區(qū)沒有請求,服務器端線程等待,直到被通知有請求了,而客戶端負責發(fā)送請求。
很顯然,我們需要對緩沖區(qū)進行保護,使得同一時刻只能有一個服務器線程在取得request,也只能同一時刻有一個客戶端線程寫入服務。
用UML描述如下:
具體實現(xiàn)可以參看代碼。
但是,這個模式有一點點瑕疵,那就是緩沖區(qū)沒有限制,對于有的情況就不會合適,比如說您的緩沖區(qū)所能占用的空間受到限制。下面的Producer Consumer Pattern應該會有所幫助。
3. Producer Consumer
Producer Consumer跟上面的Guarded Suspension很像,唯一的區(qū)別在于緩沖區(qū),Guarded Suspension模式的緩沖區(qū)沒有限制,所以,他們適用的場合也就不一樣了,很顯然,這個考慮應該基于內存是否允許。Producer Consumer的緩沖區(qū)就像一個盒子,如果裝滿了,就不能再裝東西,而等待有人拿走一些,讓后才能繼續(xù)放東西,這是個形象的描述。可以參考下面的UML,然后具體可以參看源碼。
4. Worker Thread
Worker Thread與上面的Producer-consumer模式的區(qū)別在于Producer-consumer只是專注于生產(chǎn)與消費,至于如何消費則不管理。其實Worker Thread模式是Producer-consumer與Command模式的結合。這邊簡單描述一下Command pattern。用UML就和衣很清晰的描述Command pattern。
這個模式在我們的很多MVC框架中幾乎都會用到,以后我也想寫一個關于Web應用的總結,會提到具體的應用。其實Command pattern模式的核心就是針對接口編程,然后存儲命令,根據(jù)客戶短的請求取得相應的命令,然后執(zhí)行,這個跟我們的Web請求實在是太像了,其實Struts就是這樣做的,容器相當于Client,然后控制器Servlet相當于Invoker,Action相當于ICommand,那么Receiver相當于封裝在Action中的對象了,比如Request等等。
上面描述過Command pattern之后,我們回到Worker模式。
這邊看看worker的UML:
從圖中可以看到,CommandBuffer這個緩沖區(qū)不僅僅能夠存儲命令,而且可以控制消費者WorkerThread。這就是Worker模式。下面的Sequence應該會更加明確的描述這個模式,具體可以參看代碼。
5. Thread-Per-Message
Thread-Per-Message模式是一個比較常用的模式了,如果我們有一個程序需要打開一個很大的文件,打開這個文件需要很長的時間,那么我們就可以設計讓一個線程來一行一行的讀入文件,而不是一次性的全部打開,這樣從外部看起來就不會有停頓的感覺。這個模式Future模式一起學習。
6. Read-Write-Lock
考慮這樣一種情況:有一個文件,有很多線程對他進行讀寫,當有線程在讀的時候,不允許寫,這樣是為了保證文件的一致性。當然可以很多線程一起讀,這個沒有問題。如果有線程在寫,其他線程不允許讀寫。如果要比較好的處理這種情況,我們可以考慮使用Read-Write-Lock模式。
這個模式可以如下描述:
其實這個模式的關鍵在于鎖實現(xiàn),這里有個簡單的實現(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();
}
}
其實在鎖里還可以添加優(yōu)先級之類的控制。
7. Future
Future模式是Proxy模式和Thread-Per-Message模式的結合??紤]下面的情況:
比如我們的word文檔,里頭有很多圖片在末尾,我們打開這個文檔的時候會需要同時讀取這些圖片文件,但是很明顯,如果剛剛開始就全部讀取進來的話會消耗太多的內存和時間,使得顯示出現(xiàn)停頓的現(xiàn)象。那么我們應該怎么做呢,我們可以做這樣一個對象,這個對象代表需要讀入的圖片,把這個對象放在圖片的位置上,當需要顯示這個圖片的時候,我們才真正的填充這個對象。這個就是Proxy模式了。當然Proxy不僅僅是這么個意思,Proxy的真正意思是我們之需要訪問Proxy來操作我們真正需要操作的對象,以便實現(xiàn)對客戶段的控制。
這邊先簡單描述一下Proxy模式:
當Client請求的時候,我們用Proxy代替RealObject載入,當Client真正需要getObject的時候,Proxy將調用RealObject的RealObject方法,獲得真正的RealObjct。用Sequence來描述上面這段話:
下面回到Future模式,這個模式就是我們不需要真正對象的時候,首先生成一個Proxy對象來替代,然后產(chǎn)生一個線程來讀取真正的對象,讀取結束之后將這個對象設置到Proxy中,當真正需要這個對象的時候,我們可以從Proxy中取到。如下:
具體可以參看代碼的實現(xiàn)。
8. Two-phase Termination
Two-phase Termination模式就是讓線程正常結束,也就是結束之前進行一些善后處理,釋放掉該釋放的資源,完成自己當前的任務。在Java語言中,有一個方法stop,這個方法會使當前線程結束,但是我們不應該使用這個方法,因為他將會導致災難性的后果。那么我們應該怎么做呢?這里其實上面有實現(xiàn)過,就是使用設置標志的方法來替代stop方法。具體可以查看:已經(jīng)不贊成使用的方法和代碼。
9. Thread-Specific Storage
Thread-Specific Storage模式的考慮是當資源的訪問不需要線程的通信的時候,我們可以使用這個模式,這個模式的做法是每個線程有自己的一個區(qū)域,來存儲自己的變量,然后需要的時候操作這個變量。在Java中,已經(jīng)實現(xiàn)了ThreadLocal,我們可以用他來實現(xiàn)這個模式,這邊有一個簡單的實現(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
其實多線程的問題有一個很大的麻煩就是如何控制資源的同步,就是防止當前線程的中間狀態(tài)被下一個線程看到,這個有兩個辦法可以實現(xiàn),首先,就是同時只能有一個線程在訪問,另外一個辦法就是使得資源變成非可變類,既然是不變的,大家就可以隨便訪問了。
11. Balking
考慮這樣一個情況:有一個比較好的洗手的地方,你可以按按鈕來放水,其實它旁邊還有一個傳感器,可以感受到您的手過來了,應該放水,那么如果您已經(jīng)按過按鈕,水正在放,那么傳感器的放水信號應該如何處理呢,很顯然,需要丟棄這次放水請求。反過來也一樣。
Sequence如下:
線程的學習筆記和一些總結大概就這么多了,想想這段時間的學習,花費了很多的時間,但是效果是很多東西只是從書本上看來的,實在是可惜沒有辦法真正的實踐一下,所以這些東西其實應該有更深刻的理解。希望有這么一天?。。。?