前言
什麼是線程?線程,有時被稱為輕量進程(Lightweight Process,LWP),是程序執行流的最小單元。一個標準的線程由線程 ID,當前指令指針 (PC),寄存器集合和堆棧組成。另外,線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。
一個線程可以創建和撤銷另一個線程,同一進程中的多個線程之間可以並發執行。由於線程之間的相互制約,致使線程在運行中呈現出間斷性。線程也有就緒、阻塞和運行三種基本狀態。就緒狀態是指線程具備運行的所有條件,邏輯上可以運行,在等待處理機;運行狀態是指線程佔有處理機正在運行;阻塞狀態是指線程在等待一個事件(如某個信號量),邏輯上不可執行。每一個程序都至少有一個線程,若程序只有一個線程,那就是程序本身。
進程 VS 線程
進程和線程是包含關係,但是多任務既可以由多進程實現,也可以由線程實現,還可以混合多進程+多線程。
和多線程相比,多進程的缺點是:
創建進程比創建線程開銷大很多,尤其是在 Windows 上進程間通信比線程要慢,因為線程見通信就是讀寫同一個變量,速度很快多進程的優點:
多進程穩定性比多線程高,因為在多進程情況下,一個進程的崩潰不會影響其他進程,任何一個線程崩潰會導致整個進程崩潰。多線程的應用場景
程序中出現需要等待的操作,比如網絡操作、文件 IO 等,可以利用多線程充分使用處理器資源,而不會阻塞程序中其他任務的執行程序中出現可分解的大任務,比如耗時較長的計算任務,可以利用多線程來共同完成任務,縮短運算時間程序中出現需要後臺運行的任務,比如一些監測任務、定時任務,可以利用多線程來完成生命周期及五種基本狀態
首先,看一下 Thread 類中給出的關於線程狀態的說明:
接下來在看一下 Java 中線程的生命周期較為經典的圖:
上圖中基本上囊括了 Java 中多線程各重要知識點。掌握了上圖中的各知識點,Java 中的多線程也就基本上掌握了。主要包括:
Java 線程具有五中基本狀態
新建狀態(New):當線程對象對創建後,即進入了新建狀態,如:Thread t = new MyThread();就緒狀態(Runnable):當調用線程對象的 start() 方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待 CPU 調度執行,並不是說執行了 t.start() 此線程立即就會執行;運行狀態(Running):當 CPU 開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。註:就緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;阻塞狀態(Blocked):處於運行狀態中的線程由於某種原因,暫時放棄對 CPU 的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才有機會再次被 CPU 調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分為三種:等待阻塞:運行狀態中的線程執行 wait() 方法,使本線程進入到等待阻塞狀態;同步阻塞-- 線程在獲取 synchronized 同步鎖失敗(因為鎖被其它線程所佔用),它會進入同步阻塞狀態;其他阻塞 -- 通過調用線程的 sleep() 或 join() 或發出了 I/O 請求時,線程會進入到阻塞狀態。當 sleep() 狀態超時、join() 等待線程終止或者超時、或者 I/O 處理完畢時,線程重新轉入就緒狀態。死亡狀態(Dead):線程執行完了或者因異常退出了 run() 方法,該線程結束生命周期。舉個通俗一點的例子來解釋上面五種狀態,比如上廁所:
你平時去商城上廁所,準備去上廁所就是新建狀態(new),上廁所要排隊,排隊就是就緒狀態(Runnable),有坑位了,輪到你了,拉屎就是運行狀態(Running),你拉完屎發現沒有手紙,要等待別人給你送紙過來,這個狀態就是阻塞(Blocked),等你上完廁所出來,上廁所這件事情結束了就是死亡狀態了。
注意:便秘也是阻塞狀態,你便秘太久了,別人等不及了,把你趕走,這個就是掛起,還有一種情況,你便秘了,別人等不及了,跟你說你先出去醞釀一下,5分鐘後再過來拉屎,這就是睡眠。
自定義線程的實現
處於實用的角度出發,想要使用多線程,那麼第一步就是需要知道如何實現自定義線程,因為實際開發中,需要線程完成的任務是不同的,所以我們需要根據線程任務來自定義線程,JDK 為我們的開發人員提供了三種自定義線程的方式,供實際開發中使用,來開發出符合需求的多線程程序!
以下是線程的三種實現方式,以及對每種實現的優缺點進行分析,最後是對這三種實現方式進行總結;
方式一:繼承Thread類
優點:實現簡單,只需實例化繼承類的實例,即可使用線程
缺點:擴展性不足,Java是單繼承的語言,如果一個類已經繼承了其他類,就無法通過這種方式實現自定義線程
方式二:實現 Runnable 接口
優點:
擴展性好,可以在此基礎上繼承其他類,實現其他必需的功能對於多線程共享資源的場景,具有天然的支持,適用於多線程處理一份資源的場景缺點:構造線程實例的過程相對繁瑣一點
方式三:實現Callable接口
優點:
擴展性好支持多線程處理同一份資源具備返回值以及可以拋出受檢查異常缺點:
相較於實現Runnable接口的方式,較為繁瑣線程常用方法簡單介紹
sleep()
是一個靜態方法。使當前線程(即調用該方法的線程)暫停執行一段時間,讓其他線程有機會繼續執行,但它並不釋放對象鎖,當到達指定的睡眠時間後會返回,線程處於就緒狀態,然後參與CPU調度。也就是如果有 Synchronized 同步塊,其他線程仍然不同訪問共享數據。注意該方法要捕獲異常
比如有兩個線程同時執行(沒有 Synchronized ),一個線程優先級為 MAX_PRIORITY,另一個為 MIN_PRIORITY,如果沒有 sleep() 方法,只有高優先級的線程執行完成後,低優先級的線程才能執行,但當高優先級的線程 sleep(5000) 後,低優先級就有機會執行了。
總之,sleep() 可以使低優先級的線程得到執行的機會,當然也可以讓同優先級、高優先級的線程有執行的機會。
join()
Thread 類中有一個 join() 方法,非靜態方法。此方法表示,在當前線程中 a 中,b 線程調用 join() 方法,那麼,a 線程就會釋放資源,讓給 b 線程先執行。。注意該方法也要捕獲異常。
yield()
是一個靜態方法,與 sleep() 類似,只是不能由用戶指定暫停多長時間,調用該方法會讓當前線程讓出CPU使用權,然後處於就緒狀態,線程調度會從就緒隊列裡面獲取一個優先級最高的線程,也可能會調度到剛剛讓出CPU的那個線程繼續獲取CPU執行權。
wait() 和notify()、notifyAll()
這三個方法用於協調多個線程對共享數據的存取,所以必須在 Synchronized 語句塊內使用這三個方法。前面說過Synchronized 這個關鍵字用於保護共享數據,阻止其他線程對共享數據的存取。但是這樣程序的流程就很不靈活了,如何才能在當前線程還沒退出 Synchronized 數據塊時讓其他線程也有機會訪問共享數據呢?此時就用這三個方法來靈活控制。
wait() 方法使當前線程被阻塞掛起暫停執行並釋放對象鎖標誌,讓其他線程可以進入 Synchronized 數據塊,當前線程被放入對象等待池中。當調用共享對象的 notify() 或者 notifyAll() 方法才會返回,此時返回的線程會加入到鎖標誌等待池中,只有鎖標誌等待池中的線程能夠獲取鎖標誌,如果鎖標誌等待池中沒有線程,則 notify() 不起作用。
notifyAll() 則從對象等待池中移走所有等待那個對象的線程並放到鎖標誌等待池中。
需要注意的是,當線程調用共享對象的 wait() 方法時,當前線程只會釋放當前共享對象的鎖,當前線程持有的其他共享對象的監視器鎖並不會釋放。
線程中斷
為啥需要中斷呢?下面簡單的舉例情況:
比如我們會啟動多個線程做同一件事,比如搶 12306 的火車票,我們可能開啟多個線程從多個渠道買火車票,只要有一個渠道買到了,我們會通知取消其他渠道。這個時候需要關閉其他線程;很多線程的運行模式是死循環,比如在生產者/消費者模式中,消費者主體就是一個死循環,它不停的從隊列中接受任務,執行任務,在停止程序時,我們需要一種」優雅」的方法以關閉該線程;在一些場景中,比如從第三方伺服器查詢一個結果,我們希望在限定的時間內得到結果,如果得不到,我們會希望取消該任務;上面這幾個例子線程已經在運行了,並不好去幹涉,但是可以通過中斷,告訴這個線程,你應該中斷了。比如上面的例子中的線程再收到中斷後,可以通過中斷標誌來結束線程的運行。當然,你也可以收到後,不做任何處理,這也是可以的。
在 Java 中,停止一個線程的主要機制是中斷,中斷並不是強迫終止一個線程,它是一種協作機制,是給線程傳遞一個取消信號,但是由線程來決定如何以及何時退出。
需要注意的是:在停止線程的時候,不要調用 stop 方法,該方法已經被廢棄了,並且會帶來不可預測的影響。
線程對中斷的反應
RUNNABLE:線程在運行或具備運行條件只是在等待作業系統調度WAITING/TIMED_WAITING:線程在等待某個條件或超時BLOCKED:線程在等待鎖,試圖進入同步塊NEW/TERMINATED:線程還未啟動或已結束RUNNABLE 狀態
如果線程在運行中,interrupt() 只是會設置線程的中斷標誌位,沒有任何其它作用。線程應該在運行過程中合適的位置檢查中斷標誌位,比如說,如果主體代碼是一個循環,可以在循環開始處進行檢查,如下所示:
WAITING/TIMED_WAITING
線程執行如下方法會進入WAITING狀態:
public final void join() throws InterruptedExceptionpublic final void wait() throws InterruptedException
執行如下方法會進入TIMED_WAITING狀態:
public final native void wait(long timeout) throws InterruptedException;public static native void sleep(long millis) throws InterruptedException;public final synchronized void join(long millis) throws InterruptedException
在這些狀態時,對線程對象調用 interrupt() 會使得該線程拋出 InterruptedException,需要注意的是,拋出異常後,中斷標誌位會被清空(線程的中斷標誌位會由 true 重置為false,因為線程為了處理異常已經重新處於就緒狀態),而不是被設置。比如說,執行如下代碼:
InterruptedException 是一個受檢異常,線程必須進行處理。我們在異常處理中介紹過,處理異常的基本思路是,如果你知道怎麼處理,就進行處理,如果不知道,就應該向上傳遞,通常情況下,你不應該做的是,捕獲異常然後忽略。
捕獲到 InterruptedException,通常表示希望結束該線程,線程大概有兩種處理方式:
向上傳遞該異常,這使得該方法也變成了一個可中斷的方法,需要調用者進行處理有些情況,不能向上傳遞異常,比如Thread的run方法,它的聲明是固定的,不能拋出任何受檢異常,這時,應該捕獲異常,進行合適的清理操作,清理後,一般應該調用Thread的interrupt方法設置中斷標誌位,使得其他代碼有辦法知道它發生了中斷第一種方式的示例代碼如下:
第二種方式的示例代碼如下:
BLOCKED
如果線程在等待鎖,對線程對象調用interrupt()只是會設置線程的中斷標誌位,線程依然會處於BLOCKED狀態,也就是說,interrupt()並不能使一個在等待鎖的線程真正」中斷」。我們看段代碼:
BLOCKED 如果線程在等待鎖,對線程對象調用 interrupt() 只是會設置線程的中斷標誌位,線程依然會處於 BLOCKED 狀態,也就是說,interrupt() 並不能使一個在等待鎖的線程真正」中斷」。我們看段代碼:
test 方法在持有鎖 lock 的情況下啟動線程 a,而線程 a 也去嘗試獲得鎖 lock,所以會進入鎖等待隊列,隨後 test 調用線程 a 的 interrupt 方法並等待線程線程 a 結束,線程 a 會結束嗎?不會,interrupt 方法只會設置線程的中斷標誌,而並不會使它從鎖等待隊列中出來。線程a 會一直嘗試獲取鎖,但是主線程也在等待 a 結束才會釋放鎖,所以相互之間互為等待,不能結束。
我們稍微修改下代碼,去掉 test方法中的最後一行 a.join(),即變為:
這時,程序就會退出。為什麼呢?因為主線程不再等待線程 a 結束,釋放鎖 lock 後,線程 a 會獲得鎖,然後檢測到發生了中斷,所以會退出。
在使用 synchronized 關鍵字獲取鎖的過程中不響應中斷請求,這是 synchronized 的局限性。如果這對程序是一個問題,應該使用顯式鎖,java 中的 Lock 接口,它支持以響應中斷的方式獲取鎖。對於 Lock.lock(),可以改用 Lock.lockInterruptibly(),可被中斷的加鎖操作,它可以拋出中斷異常。等同於等待時間無限長的 Lock.tryLock(long time, TimeUnit unit)。
NEW/TERMINATE
如果線程尚未啟動 (NEW),或者已經結束 (TERMINATED),則調用 interrupt() 對它沒有任何效果,中斷標誌位也不會被設置。比如說,以下代碼的輸出都是 false。
IO操作
如果線程在等待 IO 操作,尤其是網絡 IO,則會有一些特殊的處理,我們沒有介紹過網絡,這裡只是簡單介紹下。
實現此 InterruptibleChannel 接口的通道是可中斷的:如果某個線程在可中斷通道上因調用某個阻塞的 I/O 操作(常見的操作一般有這些:serverSocketChannel. accept()、socketChannel.connect、socketChannel.open、socketChannel.read、socketChannel.write、fileChannel.read、fileChannel.write)而進入阻塞狀態,而另一個線程又調用了該阻塞線程的 interrupt 方法,這將導致該通道被關閉,並且已阻塞線程接將會收到 ClosedByInterruptException,並且設置已阻塞線程的中斷狀態。另外,如果已設置某個線程的中斷狀態並且它在通道上調用某個阻塞的 I/O 操作,則該通道將關閉並且該線程立即接收到 ClosedByInterruptException;並仍然設置其中斷狀態。如果線程阻塞於 Selector 調用,則線程的中斷標誌位會被設置,同時,阻塞的調用會立即返回。我們重點介紹另一種情況,InputStream 的 read 調用,該操作是不可中斷的,如果流中沒有數據,read 會阻塞 (但線程狀態依然是 RUNNABLE ),且不響應 interrupt(),與 synchronized 類似,調用 interrupt() 只會設置線程的中斷標誌,而不會真正」中斷」它,我們看段代碼
線程t啟動後調用 System.in.read() 從標準輸入讀入一個字符,不要輸入任何字符,我們會看到,調用 interrupt() 不會中斷 read(),線程會一直運行。
不過,有一個辦法可以中斷 read() 調用,那就是調用流的 close 方法,我們將代碼改為:
我們給線程定義了一個 cancel 方法,在該方法中,調用了流的 close 方法,同時調用了 interrupt 方法,這次,程序會輸出:
-1exit
也就是說,調用close方法後,read方法會返回,返回值為-1,表示流結束。
如何正確地取消/關閉線程
1. 以上,我們可以看出,interrupt 方法不一定會真正」中斷」線程,它只是一種協作機制,如果 不明白線程在做什麼,不應該貿然的調用線程的 interrupt 方法,以為這樣就能取消線程。
2. 對於以線程提供服務的程序模塊而言,它應該封裝取消/關閉操作,提供單獨的取消/關閉方法給調用者,類似於 InterruptReadDemo 中演示的 cancel 方法,外部調用者應該調用這些方法而不是直接調用 interrupt。
3. Java並發庫的一些代碼就提供了單獨的取消/關閉方法,比如說,Future接口提供了如下方法以取消任務:
boolean cancel(boolean mayInterruptIfRunning);
4. 再比如,ExecutorService提供了如下兩個關閉方法:
void shutdown(); List<Runnable> shutdownNow();
5. Future 和 ExecutorService 的 API 文檔對這些方法都進行了詳細說明,這是我們應該學習的方式。