筆記--深入瞭解虛擬機(線程安全)

前言

Java虛擬機在千差萬別的物理機上建立來統一的運行平臺,實現了在任意一臺虛擬機上編譯的源程序能在任何一臺虛擬機上正常運行。

程序員可以把主要精力放在具體的業務邏輯上,而不是物理硬件的兼容性上。

開發人員如果不瞭解虛擬機一些技術特性的運行原理,就無法寫出最適合虛擬機運行和自優化的代碼。

線程安全

google上定義:如果一個對象可以安全地被多個線程同時使用,那它就是線程安全的

《Java Concurrency In Practice》的作者Brian Goctz對線程安全有一個比較恰當的定義:“當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那麼這個對象是線程安全的。”

按照線程安全的“安全程度”由強至弱來排序,可以將Java語言中各種操作共享的數據分爲以下5類:

  1. 不可變--不可變的對象一定是安全的。無論是對象的方法實現還是方法的調用者,都不需要再採取任何的線程安全措施保障措施。
  2. 絕對線程安全--不要任何同步保障措施也可以被多線程調用後所得的運行結果正確的類,這樣的類是線程安全的。(這裏可以與相對線程安全做比較記憶)
  3. 相對線程安全--通常講的線程安全。他需要保證對這個對象單獨的操作是線程安全的,在調用時不要加額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。需要添加同步措施synchronized(vector)在使用vector方法時。
  4. 線程兼容--指對象本身並不是線程安全的,但是可以通過在調用端正確的使用同步手段來保證對象在併發環境下可以安全地使用,我們平常說一個類不是線程安全的,絕大多數時候指的是這一種情況
  5. 線程對立--是指無論調用端是否採用同步措施,都無法在多線程環境中併發使用的代碼。Java語言天生就具備多線程特性,線程對立這種排斥多線程的代碼是很少出現的,而且通常都是有害的,應當儘量避免。例子:Thread類中的suspend()和resume()方法。存在死鎖的風險。--所以這兩個方法被廢棄了。
  • 線程安全的類:Vertor、HashTable、String/StringBuffer
  • 線程不安全類:ArrayList、HashMap、StringBuilder

線程安全與否的因素:(自己總結的)

  • 類內是否使用來final定義方法或變量
  • 編寫方法時是否採用同步機制synchronized--(這個得看一些類的源碼例如:vector)
  • 在調用方法時,是否在調用端做額外的同步措施---例如synchronized(vector)

線程安全的實現方法

實現線程安全與代碼編寫有很大的關係,但虛擬機提供的同步和鎖機制也起到了非常重要的作用

  1. 互斥同步--常見的一種併發正確性保障手段。同步是指多個線程併發的訪問共享數據時,保證共享數據在同一時刻只能被一個線程使用。互斥是同步的一種手段,臨界區、信號量、互斥量都是主要的互斥實現方式。最基本的互斥同步手段就是關鍵字synchronized。                                                                                                                                                                                             synchronized關鍵字經編譯後,會在同步塊前後生成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都需要一個reference類型的參數來指明要鎖定和解鎖的對象。例如synchronized<vertor>,若沒有指定,如synchronized<>,則根據synchronized修飾的是實例方法還是類方法,去取對應的對象實例或Class對象來作爲鎖對象。
  2. 據JVM規範的要求,在執行monitorenter指令時,首先嚐試獲取對象的鎖。如果這個對象沒有被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1,相應的,在執行monitorexit指令時會將鎖計數器減1。
虛擬機規範對monitorenter和monitorexit的行爲描述中,有兩點需要特別注意:

  1. synchronized同步塊對同一線程來說是可重入,不會出現自己把自己鎖死的情況。
  2. 同步塊在已進入的線程執行完之前,會阻塞後面其他進程的進入。
JAVA的線程是映射在操作系統原生線程之上的,如果要阻塞或喚醒一個線程都需要操作系統來幫忙,這就需要從用戶態轉換到核心態,狀態轉換需要耗費大量的處理機時間,對於簡單的代碼同步塊,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長。所以,synchronized是一個重量級的操作
JVM本身會進行一定的優化,譬如在通知操作系統阻塞線程之前加入一段自旋等待過程,避免頻繁地切入到核心態之中。

除了synchronized外還可以使用java.util.concurrent包中的重入鎖(ReentrantLock)來實現同步。

synchronized與ReenterantLock區別

代碼寫法上:synchronized表現爲API層面上的互斥鎖(lock()和unlock()方法配合try/finally語句塊來完成)

                          ReentrantLock表現爲原生語法層面的互斥鎖。

ReentrantLock增加了一些功能:

  1. 等待可中斷:是指當持有鎖的線程長時間佔用鎖資源不釋放,正在等待的線程可以選擇放棄等待,改爲處理其他事情,可中斷特性對處理執行時間非常長的同步塊很有幫助。
  2. 公平鎖:是指多個線程在等待一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的線程都有機會獲得鎖。synchronized是非公平鎖,ReentrantLock默認是非公平鎖,但可以通過帶布爾值的構造函數要求使用公平鎖。
  3. 鎖綁定多個條件:指一個ReentrantLock對象可以同時綁定多個Condition對象。
非阻塞同步

互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,因此這種同步也叫阻塞同步。

從處理問題的方式上說,互斥同步屬於一種悲觀的併發策略。

隨着硬件指令集的發展,我們有了另外一個選擇:基於衝突檢測的樂觀併發策略,通俗地說,先進行操作,若沒有其他資源爭用共享數據,那操作就成功了;如果共享數據有爭用,產生了衝突,那就採取其他的補償措施。這種樂觀的併發策略的許多實現都不需要把線程掛起,因此這種同步操作稱爲非阻塞同步

常用硬件指令:

  • 測試並設置(Test-and-Set)
  • 獲取並增加(Fetch-and-Increment)
  • 交換(Swap)
  • 比較並交換(Compare-and-Swap,CAS)---ABA問題、邏輯漏洞
  • 加載鏈接/條件存儲(Load-Linked/Store-Conditional,下文稱LL/SC)
無同步方案

要保證線程安全,並不是一定就要進行同步,兩者沒有因果關係。同步只是保證共享數據爭用時的正確性手段,如果一個方法本來就不涉及共享數據,那自然就無須任何同步措施去保證正確性,因此有些代碼天生就是安全的。簡單介紹其中的兩類:

  1. 可重入代碼(Reentrant Code):也叫純代碼(Pure Code)可以在代碼執行的任何時刻中斷它,轉而去執行另一段代碼,當控制權返回後,原來的程序不會出現任何錯誤。
  2. 線程本地存儲(Thread Local Storage): 如果一段代碼所需要的數據必須與其他代碼共享,那就看看這些共享數據的代碼能否能保證在同一個線程中執行?若能保證,我們就可以把共享數據的可見範圍限制在同一個線程之內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。

Java中如果一個變量要被多個線程訪問,可以使用volative關鍵字聲明它爲“易變的”,如果某個變量被單個線程獨享,Java中並沒有單獨的關鍵字來標識,不過可以通過java.util.ThreadLocal類來實現線程本地存儲的功能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章