1. 程式人生 > >Java併發指南1:併發基礎與Java多執行緒

Java併發指南1:併發基礎與Java多執行緒

什麼是併發

在過去單CPU時代,單任務在一個時間點只能執行單一程式。之後發展到多工階段,計算機能在同一時間點並行執行多工或多程序。雖然並不是真正意義上的“同一時間點”,而是多個任務或程序共享一個CPU,並交由作業系統來完成多工間對CPU的執行切換,以使得每個任務都有機會獲得一定的時間片執行。

隨著多工對軟體開發者帶來的新挑戰,程式不在能假設獨佔所有的CPU時間、所有的記憶體和其他計算機資源。一個好的程式榜樣是在其不再使用這些資源時對其進行釋放,以使得其他程式能有機會使用這些資源。

再後來發展到多執行緒技術,使得在一個程式內部能擁有多個執行緒並行執行。一個執行緒的執行可以被認為是一個CPU在執行該程式。當一個程式執行在多執行緒下,就好像有多個CPU在同時執行該程式。

多執行緒比多工更加有挑戰。多執行緒是在同一個程式內部並行執行,因此會對相同的記憶體空間進行併發讀寫操作。這可能是在單執行緒程式中從來不會遇到的問題。其中的一些錯誤也未必會在單CPU機器上出現,因為兩個執行緒從來不會得到真正的並行執行。然而,更現代的計算機伴隨著多核CPU的出現,也就意味著不同的執行緒能被不同的CPU核得到真正意義的並行執行。

如果一個執行緒在讀一個記憶體時,另一個執行緒正向該記憶體進行寫操作,那進行讀操作的那個執行緒將獲得什麼結果呢?是寫操作之前舊的值?還是寫操作成功之後的新值?或是一半新一半舊的值?或者,如果是兩個執行緒同時寫同一個記憶體,在操作完成後將會是什麼結果呢?是第一個執行緒寫入的值?還是第二個執行緒寫入的值?還是兩個執行緒寫入的一個混合值?因此如沒有合適的預防措施,任何結果都是可能的。而且這種行為的發生甚至不能預測,所以結果也是不確定性的。

Java的多執行緒和併發性

Java是最先支援多執行緒的開發的語言之一,Java從一開始就支援了多執行緒能力,因此Java開發者能常遇到上面描述的問題場景。這也是我想為Java併發技術而寫這篇系列的原因。作為對自己的筆記,和對其他Java開發的追隨者都可獲益的。

該系列主要關注Java多執行緒,但有些在多執行緒中出現的問題會和多工以及分散式系統中出現的存在類似,因此該系列會將多工和分散式系統方面作為參考,所以叫法上稱為“併發性”,而不是“多執行緒”。

多執行緒的優點

儘管面臨很多挑戰,多執行緒有一些優點使得它一直被使用。這些優點是:

  • 資源利用率更好
  • 程式設計在某些情況下更簡單
  • 程式響應更快                                    

資源利用率更好 

想象一下,一個應用程式需要從本地檔案系統中讀取和處理檔案的情景。比方說,從磁碟讀取一個檔案需要5秒,處理一個檔案需要2秒。處理兩個檔案則需要:

從磁碟中讀取檔案的時候,大部分的CPU時間用於等待磁碟去讀取資料。在這段時間裡,CPU非常的空閒。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用CPU資源。看下面的順序: 

CPU等待第一個檔案被讀取完。然後開始讀取第二個檔案。當第二檔案在被讀取的時候,CPU會去處理第一個檔案。記住,在等待磁碟讀取檔案的時候,CPU大部分時間是空閒的。

總的說來,CPU能夠在等待IO的時候做一些其他的事情。這個不一定就是磁碟IO。它也可以是網路的IO,或者使用者輸入。通常情況下,網路和磁碟的IO比CPU和記憶體的IO慢的多。

程式設計更簡單

在單執行緒應用程式中,如果你想編寫程式手動處理上面所提到的讀取和處理的順序,你必須記錄每個檔案讀取和處理的狀態。相反,你可以啟動兩個執行緒,每個執行緒處理一個檔案的讀取和操作。執行緒會在等待磁碟讀取檔案的過程中被阻塞。在等待的時候,其他的執行緒能夠使用CPU去處理已經讀取完的檔案。其結果就是,磁碟總是在繁忙地讀取不同的檔案到記憶體中。這會帶來磁碟和CPU利用率的提升。而且每個執行緒只需要記錄一個檔案,因此這種方式也很容易程式設計實現。

程式響應更快

將一個單執行緒應用程式變成多執行緒應用程式的另一個常見的目的是實現一個響應更快的應用程式。設想一個伺服器應用,它在某一個埠監聽進來的請求。當一個請求到來時,它去處理這個請求,然後再返回去監聽。

伺服器的流程如下所述:

如果一個請求需要佔用大量的時間來處理,在這段時間內新的客戶端就無法傳送請求給服務端。只有伺服器在監聽的時候,請求才能被接收。另一種設計是,監聽執行緒把請求傳遞給工作者執行緒(worker thread),然後立刻返回去監聽。而工作者執行緒則能夠處理這個請求併發送一個回覆給客戶端。這種設計如下所述:

這種方式,服務端執行緒迅速地返回去監聽。因此,更多的客戶端能夠傳送請求給服務端。這個服務也變得響應更快。

桌面應用也是同樣如此。如果你點選一個按鈕開始執行一個耗時的任務,這個執行緒既要執行任務又要更新視窗和按鈕,那麼在任務執行的過程中,這個應用程式看起來好像沒有反應一樣。相反,任務可以傳遞給工作者執行緒(word thread)。當工作者執行緒在繁忙地處理任務的時候,視窗執行緒可以自由地響應其他使用者的請求。當工作者執行緒完成任務的時候,它傳送訊號給視窗執行緒。視窗執行緒便可以更新應用程式視窗,並顯示任務的結果。對使用者而言,這種具有工作者執行緒設計的程式顯得響應速度更快。

多執行緒的代價

從一個單執行緒的應用到一個多執行緒的應用並不僅僅帶來好處,它也會有一些代價。不要僅僅為了使用多執行緒而使用多執行緒。而應該明確在使用多執行緒時能多來的好處比所付出的代價大的時候,才使用多執行緒。如果存在疑問,應該嘗試測量一下應用程式的效能和響應能力,而不只是猜測。

設計更復雜

雖然有一些多執行緒應用程式比單執行緒的應用程式要簡單,但其他的一般都更復雜。在多執行緒訪問共享資料的時候,這部分程式碼需要特別的注意。執行緒之間的互動往往非常複雜。不正確的執行緒同步產生的錯誤非常難以被發現,並且重現以修復。

上下文切換的開銷

當CPU從執行一個執行緒切換到執行另外一個執行緒的時候,它需要先儲存當前執行緒的本地的資料,程式指標等,然後載入另一個執行緒的本地資料,程式指標等,最後才開始執行。這種切換稱為“上下文切換”(“context switch”)。CPU會在一個上下文中執行一個執行緒,然後切換到另外一個上下文中執行另外一個執行緒。

上下文切換並不廉價。如果沒有必要,應該減少上下文切換的發生。

你可以通過維基百科閱讀更多的關於上下文切換相關的內容:

增加資源消耗

執行緒在執行的時候需要從計算機裡面得到一些資源。除了CPU,執行緒還需要一些記憶體來維持它本地的堆疊。它也需要佔用作業系統中一些資源來管理執行緒。我們可以嘗試編寫一個程式,讓它建立100個執行緒,這些執行緒什麼事情都不做,只是在等待,然後看看這個程式在執行的時候佔用了多少記憶體。

競態條件與臨界區

在同一程式中執行多個執行緒本身不會導致問題,問題在於多個執行緒訪問了相同的資源。如,同一記憶體區(變數,陣列,或物件)、系統(資料庫,web services等)或檔案。實際上,這些問題只有在一或多個執行緒向這些資源做了寫操作時才有可能發生,只要資源沒有發生變化,多個執行緒讀取相同的資源就是安全的。

多執行緒同時執行下面的程式碼可能會出錯:

 

想象下執行緒A和B同時執行同一個Counter物件的add()方法,我們無法知道作業系統何時會在兩個執行緒之間切換。JVM並不是將這段程式碼視為單條指令來執行的,而是按照下面的順序:

從記憶體獲取 this.count 的值放到暫存器
將暫存器中的值增加value
將暫存器中的值寫回記憶體

觀察執行緒A和B交錯執行會發生什麼:

         this.count = 0;
   A:    讀取 this.count 到一個暫存器 (0)
   B:    讀取 this.count 到一個暫存器 (0)
   B:     將暫存器的值加2
   B:    回寫暫存器值(2)到記憶體. this.count 現在等於 2
   A:    將暫存器的值加3
   A:    回寫暫存器值(3)到記憶體. this.count 現在等於 3

兩個執行緒分別加了2和3到count變數上,兩個執行緒執行結束後count變數的值應該等於5。然而由於兩個執行緒是交叉執行的,兩個執行緒從記憶體中讀出的初始值都是0。然後各自加了2和3,並分別寫回記憶體。最終的值並不是期望的5,而是最後寫回記憶體的那個執行緒的值,上面例子中最後寫回記憶體的是執行緒A,但實際中也可能是執行緒B。如果沒有采用合適的同步機制,執行緒間的交叉執行情況就無法預料。

競態條件&臨界區 

當兩個執行緒競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件。導致競態條件發生的程式碼區稱作臨界區。上例中add()方法就是一個臨界區,它會產生競態條件。在臨界區中使用適當的同步就可以避免競態條件。

執行緒安全與共享資源

允許被多個執行緒同時執行的程式碼稱作執行緒安全的程式碼。執行緒安全的程式碼不包含競態條件。當多個執行緒同時更新共享資源時會引發競態條件。因此,瞭解Java執行緒執行時共享了什麼資源很重要。

區域性變數

區域性變數儲存線上程自己的棧中。也就是說,區域性變數永遠也不會被多個執行緒共享。所以,基礎型別的區域性變數是執行緒安全的。下面是基礎型別的區域性變數的一個例子:

區域性的物件引用 

物件的區域性引用和基礎型別的區域性變數不太一樣。儘管引用本身沒有被共享,但引用所指的物件並沒有儲存線上程的棧內。所有的物件都存在共享堆中。如果在某個方法中建立的物件不會逃逸出(譯者注:即該物件不會被其它方法獲得,也不會被非區域性變數引用到)該方法,那麼它就是執行緒安全的。實際上,哪怕將這個物件作為引數傳給其它方法,只要別的執行緒獲取不到這個物件,那它仍是執行緒安全的。下面是一個執行緒安全的區域性引用樣例:

樣例中LocalObject物件沒有被方法返回,也沒有被傳遞給someMethod()方法外的物件。每個執行someMethod()的執行緒都會建立自己的LocalObject物件,並賦值給localObject引用。因此,這裡的LocalObject是執行緒安全的。事實上,整個someMethod()都是執行緒安全的。即使將LocalObject作為引數傳給同一個類的其它方法或其它類的方法時,它仍然是執行緒安全的。當然,如果LocalObject通過某些方法被傳給了別的執行緒,那它就不再是執行緒安全的了。

物件成員 

物件成員儲存在堆上。如果兩個執行緒同時更新同一個物件的同一個成員,那這個程式碼就不是執行緒安全的。下面是一個樣例:

如果兩個執行緒同時呼叫同一個NotThreadSafe例項上的add()方法,就會有競態條件問題。例如:

 

 

注意兩個MyRunnable共享了同一個NotThreadSafe物件。因此,當它們呼叫add()方法時會造成競態條件。

當然,如果這兩個執行緒在不同的NotThreadSafe例項上呼叫call()方法,就不會導致競態條件。下面是稍微修改後的例子:

現在兩個執行緒都有自己單獨的NotThreadSafe物件,呼叫add()方法時就會互不干擾,再也不會有競態條件問題了。所以非執行緒安全的物件仍可以通過某種方式來消除競態條件。 

執行緒控制逃逸規則

執行緒控制逃逸規則可以幫助你判斷程式碼中對某些資源的訪問是否是執行緒安全的。

如果一個資源的建立,使用,銷燬都在同一個執行緒內完成,
且永遠不會脫離該執行緒的控制,則該資源的使用就是執行緒安全的。

資源可以是物件,陣列,檔案,資料庫連線,套接字等等。Java中你無需主動銷燬物件,所以“銷燬”指不再有引用指向物件。

即使物件本身執行緒安全,但如果該物件中包含其他資源(檔案,資料庫連線),整個應用也許就不再是執行緒安全的了。比如2個執行緒都建立了各自的資料庫連線,每個連線自身是執行緒安全的,但它們所連線到的同一個資料庫也許不是執行緒安全的。比如,2個執行緒執行如下程式碼:

檢查記錄X是否存在,如果不存在,插入X

如果兩個執行緒同時執行,而且碰巧檢查的是同一個記錄,那麼兩個執行緒最終可能都插入了記錄:

執行緒1檢查記錄X是否存在。檢查結果:不存在
執行緒2檢查記錄X是否存在。檢查結果:不存在
執行緒1插入記錄X
執行緒2插入記錄X

同樣的問題也會發生在檔案或其他共享資源上。因此,區分某個執行緒控制的物件是資源本身,還是僅僅到某個資源的引用很重要。

執行緒安全及不可變性

當多個執行緒同時訪問同一個資源,並且其中的一個或者多個執行緒對這個資源進行了寫操作,才會產生競態條件。多個執行緒同時讀同一個資源不會產生競態條件。

我們可以通過建立不可變的共享物件來保證物件線上程間共享時不會被修改,從而實現執行緒安全。如下示例:

 

 

請注意ImmutableValue類的成員變數value是通過建構函式賦值的,並且在類中沒有set方法。這意味著一旦ImmutableValue例項被建立,value變數就不能再被修改,這就是不可變性。但你可以通過getValue()方法讀取這個變數的值。

譯者注:注意,“不變”(Immutable)和“只讀”(Read Only)是不同的。當一個變數是“只讀”時,變數的值不能直接改變,但是可以在其它變數發生改變的時候發生改變。比如,一個人的出生年月日是“不變”屬性,而一個人的年齡便是“只讀”屬性,但是不是“不變”屬性。隨著時間的變化,一個人的年齡會隨之發生變化,而一個人的出生年月日則不會變化。這就是“不變”和“只讀”的區別。(摘自《Java與模式》第34章)

如果你需要對ImmutableValue類的例項進行操作,可以通過得到value變數後建立一個新的例項來實現,下面是一個對value變數進行加法操作的示例:

 

 

請注意add()方法以加法操作的結果作為一個新的ImmutableValue類例項返回,而不是直接對它自己的value變數進行操作。

引用不是執行緒安全的!

重要的是要記住,即使一個物件是執行緒安全的不可變物件,指向這個物件的引用也可能不是執行緒安全的。看這個例子:

 

Calculator類持有一個指向ImmutableValue例項的引用。注意,通過setValue()方法和add()方法可能會改變這個引用。因此,即使Calculator類內部使用了一個不可變物件,但Calculator類本身還是可變的,因此Calculator類不是執行緒安全的。換句話說:ImmutableValue類是執行緒安全的,但使用它的類不是。當嘗試通過不可變性去獲得執行緒安全時,這點是需要牢記的。

要使Calculator類實現執行緒安全,將getValue()、setValue()和add()方法都宣告為同步方法即可。

Java多執行緒基礎

1 執行緒與多執行緒

執行緒是什麼? 執行緒(Thread)是一個物件(Object)。用來幹什麼?Java 執行緒(也稱 JVM 執行緒)是 Java 程序內允許多個同時進行的任務。該程序內併發的任務成為執行緒(Thread),一個程序裡至少一個執行緒。

Java 程式採用多執行緒方式來支援大量的併發請求處理,程式如果在多執行緒方式執行下,其複雜度遠高於單執行緒序列執行。那麼多執行緒:指的是這個程式(一個程序)執行時產生了不止一個執行緒。

為啥使用多執行緒?

  • 適合多核處理器。一個執行緒執行在一個處理器核心上,那麼多執行緒可以分配到多個處理器核心上,更好地利用多核處理器。
  • 防止阻塞。將資料一致性不強的操作使用多執行緒技術(或者訊息佇列)加快程式碼邏輯處理,縮短響應時間。

聊到多執行緒,多半會聊併發與並行,咋理解並區分這兩個的區別呢?

  • 類似單個 CPU ,通過 CPU 排程演算法等,處理多個任務的能力,叫併發
  • 類似多個 CPU ,同時並且處理相同多個任務的能力,叫做並行

 2 執行緒的執行與建立

    2.1 執行緒的建立

Java 建立執行緒物件有兩種方法:

  • 繼承 Thread 類建立執行緒物件
  • 實現 Runnable 介面類建立執行緒物件

新建 MyThread 物件,程式碼如下: 

MyThread 類繼承了 Thread 物件,並重寫(Override)了 run 方法,實現執行緒裡面的邏輯。main 函式是使用 for 語句,迴圈建立了 10 個執行緒,呼叫 start 方法啟動執行緒,最後列印當前執行緒物件的 ID。

run 方法和 start 方法的區別是什麼呢? run 方法就是跑的意思,執行緒啟動後,會呼叫 run 方法。 start 方法就是啟動的意思,就是啟動新執行緒例項。啟動執行緒後,才會調執行緒的 run 方法。

執行 main 方法後,控制檯列印如下:

可見,執行緒的 ID 是執行緒唯一識別符號,每個執行緒 ID 都是不一樣的。

start 方法和 run 方法的關係如圖所示: 

 

同理,實現 Runnable 介面類建立執行緒物件也很簡單,只是不同的形式。新建 MyThreadBrother 程式碼如下:

 

2.1 執行緒的執行 

在執行上面兩個小 demo 後,JVM 執行了 main 函式執行緒,然後在主執行緒中執行建立了新的執行緒。正常情況下,所有執行緒執行到執行結束為止。除非某個執行緒中呼叫了 System.exit(1) 則被終止。

在實際開發中,一個請求到響應式是一個執行緒。但在這個執行緒中可以使用執行緒池建立新的執行緒,去執行任務。

3 執行緒的狀態 

執行程式碼列印如下:

 

執行緒是一個物件,它有唯一識別符號 ID、名稱、狀態、優先順序等屬性。執行緒只能修改其優先順序和名稱等屬性 ,無法修改 ID 、狀態。ID 是 JVM 分配的,名字預設也為 Thread-XX,XX是一組數字。執行緒初始狀態為 NEW。 

執行緒優先順序的範圍是 1 到 10 ,其中 1 是最低優先順序,10 是最高優先順序。不推薦改變執行緒的優先順序,如果業務需要,自然可以修改執行緒優先順序到最高,或者最低。

執行緒的狀態實現通過 Thread.State 常量類實現,有 6 種執行緒狀態:new(新建)、runnnable(可執行)、blocked(阻塞)、waiting(等待)、time waiting (定時等待)和 terminated(終止)。狀態轉換圖如下:

執行緒狀態流程大致如下:

4 小結 

本文介紹了執行緒與多執行緒的基礎篇,包括了執行緒啟動及執行緒狀態等。下一篇我們聊下執行緒的具體操作。包括中斷、終止等。

執行緒中斷和終止 

    1 執行緒中斷

    1.1 什麼是執行緒中斷?

  • 執行緒建立後,進入 new 狀態
  • 呼叫 start 或者 run 方法,進入 runnable 狀態
  • JVM 按照執行緒優先順序及時間分片等執行 runnable 狀態的執行緒。開始執行時,進入 running 狀態
  • 如果執行緒執行 sleep、wait、join,或者進入 IO 阻塞等。進入 wait 或者 blocked 狀態
  • 執行緒執行完畢後,執行緒被執行緒佇列移除。最後為 terminated 狀態。

執行緒中斷是執行緒的標誌位屬性。而不是真正終止執行緒,和執行緒的狀態無關。執行緒中斷過程表示一個執行中的執行緒,通過其他執行緒呼叫了該執行緒的 interrupt() 方法,使得該執行緒中斷標誌位屬性改變。

深入思考下,執行緒中斷不是去中斷了執行緒,恰恰是用來通知該執行緒應該被中斷了。具體是一個標誌位屬性,到底該執行緒生命週期是去終止,還是繼續執行,由執行緒根據標誌位屬性自行處理。

    1.2 執行緒中斷操作

/**
 * 一直執行的執行緒,中斷狀態為 true
 *
 * @author Jeff Lee @ bysocket.com
 * @since 2018年02月23日19:03:02
 */
public class InterruptedThread implements Runnable {

    @Override // 可以省略
    public void run() {
        // 一直 run
        while (true) {
        }
    }

    public static void main(String[] args) throws Exception {

        Thread interruptedThread = new Thread(new InterruptedThread(), "InterruptedThread");
        interruptedThread.start();

        TimeUnit.SECONDS.sleep(2);

        interruptedThread.interrupt();
        System.out.println("InterruptedThread interrupted is " + interruptedThread.isInterrupted());

        TimeUnit.SECONDS.sleep(2);
    }
}

執行 main 函式,結果如下:

    InterruptedThread interrupted is true。

程式碼詳解:

另一種情況,新建 InterruptedException 物件,程式碼如下: 

  • 執行緒一直在執行狀態,沒有停止或者阻塞等
  • 呼叫了 interrupt() 方法,中斷狀態置為 true,但不會影響執行緒的繼續執行。
/**
 * 丟擲 InterruptedException 的執行緒,中斷狀態被重置為預設狀態 false
 *
 * @author Jeff Lee @ bysocket.com
 * @since 2018年02月23日19:03:02
 */
public class InterruptedException implements Runnable {

    @Override // 可以省略
    public void run() {
        // 一直 sleep
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (java.lang.InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {

        Thread interruptedThread = new Thread(new InterruptedException(), "InterruptedThread");
        interruptedThread.start();

        TimeUnit.SECONDS.sleep(2);

        // 中斷被阻塞狀態(sleep、wait、join 等狀態)的執行緒,會丟擲異常 InterruptedException
        // 在丟擲異常 InterruptedException 前,JVM 會先將中斷狀態重置為預設狀態 false
        interruptedThread.interrupt();
        System.out.println("InterruptedThread interrupted is " + interruptedThread.isInterrupted());
        TimeUnit.SECONDS.sleep(2);
    }
}

執行 main 函式,結果如下:

程式碼詳解:

  • 中斷被阻塞狀態(sleep、wait、join 等狀態)的執行緒,會丟擲異常 InterruptedException
  • 丟擲異常 InterruptedException 前,JVM 會先將中斷狀態重置為預設狀態 false

小結下執行緒中斷:

2 執行緒終止 

比如在 IDEA 中強制關閉程式,立即停止程式,不給程式釋放資源等操作,肯定是不正確的。執行緒終止也存在類似的問題,所以需要考慮如何終止執行緒?

  • 執行緒中斷,不是停止執行緒,只是一個執行緒的標誌位屬性
  • 如果執行緒狀態為被阻塞狀態(sleep、wait、join 等狀態),執行緒狀態退出被阻塞狀態,丟擲異常 InterruptedException,並重置中斷狀態為預設狀態 false
  • 如果執行緒狀態為執行狀態,執行緒狀態不變,繼續執行,中斷狀態置為 true。

比如在 IDEA 中強制關閉程式,立即停止程式,不給程式釋放資源等操作,肯定是不正確的。執行緒終止也存在類似的問題,所以需要考慮如何終止執行緒?

上面聊到了執行緒中斷,可以利用執行緒中斷標誌位屬性來安全終止執行緒。同理也可以使用 boolean 變數來控制是否需要終止執行緒。

新建 ,程式碼如下:

/**
 * 安全終止執行緒
 *
 * @author Jeff Lee @ bysocket.com
 * @since 2018年02月23日19:03:02
 */
public class ThreadSafeStop {

    public static void main(String[] args) throws Exception {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
        // 睡眠 1 秒,通知 CountThread 中斷,並終止執行緒
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();

        Runner two = new Runner();
        countThread = new Thread(two,"CountThread");
        countThread.start();
        // 睡眠 1 秒,然後設定執行緒停止狀態,並終止執行緒
        TimeUnit.SECONDS.sleep(1);
        two.stopSafely();
    }

    private static class Runner implements Runnable {

        private long i;

        // 終止狀態
        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()) {
                // 執行緒執行具體邏輯
                i++;
            }
            System.out.println("Count i = " + i);
        }

        public void stopSafely() {
            on = false;
        }
    }
}

從上面程式碼可以看出,通過 while (on && !Thread.currentThread().isInterrupted()) 程式碼來實現執行緒是否跳出執行邏輯,並終止。但是疑問點就來了,為啥需要 onisInterrupted() 兩項一起呢?用其中一個方式不就行了嗎?答案在下面

  • 執行緒成員變數 on 通過 volatile 關鍵字修飾,達到執行緒之間可見,從而實現執行緒的終止。但當執行緒狀態為被阻塞狀態(sleep、wait、join 等狀態)時,對成員變數操作也阻塞,進而無法執行安全終止執行緒
  • 為了處理上面的問題,引入了 isInterrupted(); 只去解決阻塞狀態下的執行緒安全終止。
  • 兩者結合是真的沒問題了嗎?不是的,如果是網路 io 阻塞,比如一個 websocket 一直再等待響應,那麼直接使用底層的 close 。

3 小結 

很多好友介紹,如果用 Spring 棧開發到使用執行緒或者執行緒池,那麼儘量使用框架這塊提供的執行緒操作及框架提供的終止等

Threadlocal介紹

還沒具體學習。後續學習。