Java多線程編程必備基礎知識

2020-12-08 計算機java編程

前言

什麼是線程?線程,有時被稱為輕量進程(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 文檔對這些方法都進行了詳細說明,這是我們應該學習的方式。

相關焦點

  • 大數據基礎:Java多線程入門
    在大數據開發學習當中,Java基礎是非常重要的一部分,打好了Java基礎,才能在後續的大數據框架技術學習階段,也能有所主力。而Java當中的一個重要知識點,就是多線程。今天的大數據基礎分享,我們就主要來講講Java多線程入門基礎。
  • 新手編程:Java多線程中Thread與Runnable的區別
    Java多線程中Thread與Runnable的區別定義extends Thread子類繼承Thread具備多線程能力,可以實現多線程;啟動線程的方法:①創建子類對象 ②對象名.start();不建議使用:避免多線程OOP單繼承的局限性(OOP:Object Oriented Programming,面向對象的編程、類似的還有OOD
  • Java 並發編程之美-線程相關的基礎知識
    借用 Java 並發編程實踐中的話:編寫正確的程序並不容易,而編寫正常的並發程序就更難了;相比於順序執行的情況,多線程的線程安全問題是微妙而且出乎意料的
  • java入門必備書籍
    Java多線程編程、Java網絡通信編程和Java反射機制。共覆蓋了java.awt、java.lang、java.io和java.nio、java.sql、java.text、java.util、javax.swing包下絕大部分類和接口。只要你真正想學習Java,你翻開書看上十多分鐘,你絕對會被這本書吸引,介紹操作的部分,非常具體。2.Java2入門經典
  • java線程的基礎問題講解
    1.1並發編程的目的:並發編程是為了讓程序運行得更快,當 並不是啟動更多的線程就能讓程序最大限度地並發執行,受限於死鎖和上下文切換問題。所以上下文切換會影響線程的執行速度。join 的作用多線程一定比單線程快嗎?不一定,在測試中並發數量沒超過百萬次的時候,串行比並發速度更快,因為線程有線程的創建和上下文切換的開銷。
  • 給Java新手的一些建議——Java知識點歸納(Java基礎部分)
    JVM相關(包括了各個版本的特性)對於剛剛接觸Java的人來說,JVM相關的知識不一定需要理解很深,對此裡面的概念有一些簡單的了解即可。不過對於一個有著3年以上Java經驗的資深開發者來說,不會JVM幾乎是不可接受的。JVM作為java運行的基礎,很難相信對於JVM一點都不了解的人可以把java語言吃得很透。
  • 2020年最新Java學習路線圖,剛入門編程行業的小白必備知識清單!
    Java技術可謂博大精深,知識體系非常豐富並且也極其複雜,因此想要學習好java其實並不是一件非常輕鬆的事。當然,剛跨入編程行業的小白也無需擔心,2020年最新路線圖中的知識你學完一半基本就可以找個非常不錯的開發工作了,如果想要高薪,那就默默地全部學完吧!
  • JAVA並發編程:線程並發工具類Callable、Future 和FutureTask的使用
    2、代碼示例package cn.lspj.ch2.future;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException
  • Java基礎知識點面試手冊(線程+JDK8)
    此為下篇,內容包括:高並發編程,Java8新特性。高並發編程多線程和單線程的區別和聯繫:答:在單核 CPU 中,將 CPU 分為很小的時間片,在每一時刻只能有一個線程在執行,是一種微觀上輪流佔用 CPU 的機制。
  • 【堪稱經典】JAVA多線程和並發基礎面試問答
    進程和線程之間有什麼不同?一個進程是一個獨立(self contained)的運行環境,它可以被看作一個程序或者一個應用。而線程是在進程中執行的一個任務。Java運行環境是一個包含了不同的類和程序的單一進程。線程可以被稱為輕量級進程。線程需要較少的資源來創建和駐留在進程中,並且可以共享進程中的資源。2. 多線程編程的好處是什麼?
  • Java並發編程學習前期知識上篇
    Java並發編程-前期準備知識-上我們先來看看幾個大廠真實的面試題:從上面幾個真實的面試問題來看,我們可以看到大廠的面試都會問到並發相關的問題。所以Java並發,這個無論是面試還是在工作中,並發都是會遇到的。
  • Java面試題-多線程篇十三
    線程是作業系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。程式設計師可以通過它進行多處理器編程,你可以使用多線程對運算密集型任務提速。比如,如果一個線程完成一個任務要100毫秒,那麼用十個線程完成改任務只需10毫秒。122,線程和進程有什麼區別?
  • 原創】Java並發編程系列01|開篇獲獎感言
    如今硬體的驅動和網際網路行業的飛速發展,64核的處理器已經是很常見了,大型互聯廠商的系統並發量輕鬆過百萬,傳統的中間件和資料庫肯定是不能幫我們遮風避雨了,我們只能通過並發編程來解決這些問題。所以,並發編程已經成為一項必備技能。
  • 實現多線程的標準操作,基於Runnable接口實現java多線程
    1 為什麼要用Runnable上一篇文章介紹了通過繼承Thread類,實現java多線程。但如果當我們創建的這個線程類還想繼承其他類的時候,這種方法就略顯局限了。這也是java單繼承的局限性。為了避免這種局限性,所以又提供了第二種多線程主體定義的形式:實現Runnable接口。
  • java基礎|驗證ArrayList的線程不安全
    javaDEMO本網站記錄了最全的各種JavaDEMO ,保證下載,複製就是可用的,包括基礎的, 集合的, spring的, Mybatis的等等各種
  • Java程式設計師必備基礎:Java代碼是怎麼運行的?
    運行時創建對象 方法調用,執行引擎解釋為機器碼 CPU執行指令 多線程切換上下文 編譯 我們都知道,java代碼是運行在Java虛擬機上的。
  • Java四大名著是什麼?java程式設計師提高技能的經典編程書籍推薦
    java編程的小夥伴有參考。學任何語言,基本的語法知識不能少,首推,Java四大名著( java編程思想+Effective java中文版+Java核心技術卷12),下面來具體介紹以下包含Java四大名著在內的java程式設計師類圖書。
  • JAVA多線程 集合同步
    多線程操作ListpublicclassArrayListSynchronization {publicstaticvoidmain(String[] args) {finalList<Integer> arrayList = new ArrayList<Integer>();finalList<Integer>
  • 40個Java多線程問題總結
    前言個人認為,學習,內容越多、越雜的知識,越需要進行深刻的總結,這樣才能記憶深刻,將知識變成自己的。這篇文章主要是對多線程的問題進行總結的,因此羅列了40個多線程的問題。這些多線程的問題,有些來源於各大網站、有些來源於自己的思考。
  • 「原創」Java並發編程系列09|基礎乾貨
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第9篇。現在,我們進入正題:介紹並發編程的基礎性概念。