Java多執行緒程式設計基礎

FuyunWang發表於2018-02-22

Java中的執行緒可以分成守護執行緒和使用者執行緒,使用者執行緒會阻止JVM的正常停止,只有當應用程式中的所有使用者執行緒全部停止完畢的時候JVM才會正常停止;相反,守護執行緒則不會影響JVM的正常停止。因此守護執行緒通常用於執行一些重要性不是很高的任務,例如監視JVM中其他執行緒的執行狀況。

Java中,建立一個執行緒就是建立一個Thread類的例項。JVM會為一個Thread例項分配兩個呼叫棧所需的記憶體空間,其中一個呼叫棧用於跟蹤Java程式碼之間的呼叫關係,另外一個呼叫棧用於跟蹤Java程式碼對原生程式碼(Native程式碼主要是C程式碼)的呼叫關係;這兩個呼叫棧也對應於兩個執行緒,一個是JVM中的執行緒(Java執行緒),另外一個是與JVM中的執行緒相對應的依賴於JVM宿主機作業系統的本地系統。呼叫Thread例項的start方法來啟動一個Thread例項,之後當相應的執行緒被JVM的執行緒排程器排程到的時候相應的執行緒中的run方法執行。

Java中子執行緒是否是一個守護執行緒取決於父執行緒。預設的情況下,父執行緒是使用者執行緒子執行緒也是使用者執行緒,父執行緒是守護執行緒則子執行緒也是守護執行緒。此外可以通過呼叫Thread例項的setDaemon方法來修改執行緒的這一屬性。

Java執行緒的狀態可以通過呼叫相應的Thread類的例項的getState()獲取,返回型別Thread.State是一個列舉型別。Java執行緒的狀態分成下面幾種:

  1. NEW:一個剛建立而未啟動的執行緒處於該狀態,每一個執行緒只會有一次處於該狀態。
  2. RUNNABLE:該狀態可以看成是一個複合的狀態。其又包括兩個子狀態,READY:表示處於該狀態的執行緒可以被JVM的執行緒任務器呼叫其進行排程使之處於RUNNING狀態,RUNNING表示出於這個狀態的執行緒正在執行。呼叫Thread例項的yield方法可以使執行緒的狀態由RUNNING轉變成READY。
  3. BLOCKED:一個執行緒發起一個阻塞式IO操作之後,或者檢視去獲得一個其他執行緒的持有的鎖時,相應的執行緒會處於BLOCKED狀態。
  4. WAITING:一個執行緒例項呼叫了wait()、join()、park()等方法之後會處於這種等待其他執行緒執行的狀態。通過呼叫notify()、notifyAll()、unpark()方法會使執行緒由WAITING狀態轉換成RUNNABLE狀態。
  5. TIMED_WAITING:此執行緒狀態區別於WAITING狀態在於該狀態下的執行緒有一定的時間限制。當超過指定的時間限制之後執行緒自動會由WAITING狀態轉換成RUNNAED狀態。
  6. TERMINATED:處於該狀態的執行緒表示已經執行結束。

下面介紹關於JAVA的記憶體模型

  1. Java所有變數都儲存在主記憶體中
  2. 每個執行緒都有自己獨立的工作記憶體,裡面儲存該執行緒的使用到的變數副本(該副本就是主記憶體中該變數的一份拷貝)
  3. 執行緒對共享變數的操作都是在自己的記憶體中完成,而不是在主記憶體中完成。
  4. 執行緒對共享變數的操作預設情況下在其他執行緒中不可見,可以通過將本地執行緒的變數同步到共享記憶體中之後將共享變數同步到其他的執行緒

記憶體模型如下:

Aaron Swartz

下面介紹在Java多執行緒程式設計中的幾個常見特性:原子性、記憶體可見性和重排序。

原子性:原子(Atomic)操作指相應的操作是單一不可分割的操作。

在多執行緒中,非原子操作可能會受到其他執行緒的干擾,使用關鍵字synchronized可以實現操作的原子性。synchronized的本質是通過該關鍵字所包括的臨界區的排他性保證在任何一個時刻只有一個執行緒能夠執行臨界區中的程式碼,從而使的臨界區中的程式碼實現了原子操作。

記憶體可見性:CPU在執行程式碼時,為了減少變數訪問的時間消耗會將程式碼中訪問的變數值快取到CPU的快取區中,程式碼在訪問某個變數時,相應的值會從快取中讀取而不是在主記憶體中讀取;同樣的,程式碼對被快取過的變數的值的修改可能僅僅是寫入快取區而不是寫回到記憶體中。這樣就導致一個執行緒對相同變數的修改無法同步到其他執行緒從而導致了記憶體的不可見性。

可以使用synchronizedvolatile來解決記憶體的不可見性問題。兩者又有點不同。synchronized仍然是 通過將程式碼在臨界區中對變數進行改變,然後使得對稍後執行該臨界區中程式碼的執行緒是可見的。volatile不同之處在於,一個執行緒對一個採用volatile關鍵字修飾的變數的值的更改對於其他使用該變數的執行緒總是可見的,它是通過將變數的更改直接同步到主記憶體中,同時其他執行緒快取中的對應變數失效,從而實現了變數的每次讀取都是從主記憶體中讀取。

指令重排序:編譯器和CPU為了提高指令的執行效率,經常會將指令進行重排序,指令的重排序導致程式碼的執行順序改變,這經常會導致一系列的問題,比如在物件的建立過程中,指令的重排序使得我們得到了一個已經分配好的記憶體而物件的初始化並未完成,從而導致空指標的異常。volatile關鍵字可以禁止指令的重排序從而解決這類問題。

總之,synchronized可以保證在多執行緒中操作的原子性和記憶體可見性,但是會引起上下文切換;而volatile關鍵字僅能保證記憶體可見性,但是可以禁止指令的重排序,同時不會引起上下文切換。

相關文章