線程安全與鎖優化,全是乾貨!

線程安全

當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行其他任何的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象就是線程安全的。

java語言中的線程安全

爲了更加深入的理解線程安全,在這裏我們可以不把線程安全當做一個非真即假的二元排他選項來看待,按照線程安全的“安全程度”由弱至強來排序,我們可以將java語言中各種操作共享的數據分爲以下5類:

1、不可變

在java語言中(特指JDK1.5以後,即java內存模型被修正之後的java語言),不可變(Immutable)的對象一定是線程安全的,無論是對象的方法實現還是方法的調用者,都不需要再採取任何的線程安全保障措施。

如果共享數據是一個基本數據類型,那麼只要在定義時使用final關鍵字修飾它就可以保證它是不可變的。如果共享數據是一個對象,那就需要保證對象的行爲不會對其狀態產生任何影響纔行,保證對象行爲不影響自己狀態的途徑有很多種,其中最簡單的就是把對象中帶有狀態的變量都聲明爲final。例子:java.lang.String類,java.lang.integer類

2、絕對線程安全

在java API中標註自己是線程安全的類,大多數都不是絕對的線程安全。

3、相對線程安全

相對的線程安全就是我們通常意義上所講的線程安全,它需要保證這個對象單獨的操作是線程安全的,我們在調用的時候不需要做額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。例如:Vector、HashTable、Collectiongs的synchronizedCollection()方法包裝的集合等。

4、線程兼容

線程兼容是指對象本身並不是線程安全的,但是可以通過在調用端正確的使用同步手段來保證對象在併發環境中可以安全的使用,我們平常說一個類不是線程安全的,覺大多數時候指的是這一種情況。

5、線程對立

線程對立是指無論調用端是否採用同步措施,都無法在多線程環境中併發使用的的代碼。一個線程對立的例子就是Thread類的suspend()和resume()方法。也正是這個原因,所以被jdk聲明廢棄了。常見的線程對立操作還有System.setIn、System.setOut和System.runFinalizersOnExit()等。

線程安全的實現方法

1、互斥同步

同步是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個(或者是一些,使用信號量的時候)線程使用。而互斥是實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和信號量(Semaphore)都是主要的互斥實現方式。

synchronized關鍵字

synchronized關鍵字經過編譯後會在同步塊前後分別形成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都需要一個Reference類型的參數來指明要鎖定和解鎖的對象。如果指定了對象參數,那就是這個對象的Reference;如果沒有明確指定,那就根據修飾的是實例方法還是類方法,去取對應的對象實例或class對象來作爲鎖對象。

虛擬機規範對monitorenter和monitorexit的行爲描述中,有兩點需要注意

  • synchronized同步塊對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題
  • 同步塊在已進入的線程執行完之前,會阻塞後面其他線程的進入
    • java的線程是映射到操作系統的原生線程上,如果要阻塞或喚醒一個線程,都需要操作系統來幫忙完成,這就需要從用戶態轉換到核心態,因此狀態切換會耗費處理器時間
    • 對於簡單的同步塊,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長
    • 虛擬機本身也會進行一些優化,譬如在通知操作系統阻塞線程之前會加入一段自旋等待過程,避免頻繁的切入到核心態中

重入鎖ReetrantLock

相比synchornized,ReentranLock增加了一些高級功能,主要有以下3項:

  • 等待可中斷:當前持有鎖的線程如果長時間不釋放鎖,正在等待的線程可以選擇放棄等待,改爲處理其他事情
  • 公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖;synchronized中的鎖是非公平的,ReentrantLock默認情況下也是非公平的,但可以通過帶布爾值的構造函數要求使用公平鎖
  • 鎖綁定多個條件:一個ReentrantLock對象可以同時綁定多個Condition對象,而在synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實現一個隱含條件,如果要和多餘一個的條件關聯,就不得不額外添加一個鎖
2、非阻塞同步

隨着硬件指令集的發展,我們除了互斥同步這種“悲觀的”併發策略,我們還有另外一個選擇:基於衝突檢測的樂觀併發策略,通俗的說,就是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果共享數據有爭用,產生衝突,那就再採取其他的補償措施(最常見的補償措施就是不斷重試,知道成功爲止),這種樂觀的併發策略的許多實現都不需要把線程掛起,因此這種同步操作稱爲非阻塞同步(Non-Blocking Synchronization)。

CAS原子操作https://blog.csdn.net/cringkong/article/details/80533917

3、無同步方案

要保證線程安全,並不一定就要進行同步,兩者沒有因果關係。

可重入代碼

這種代碼也叫作純代碼(Pure Code),可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼(包括遞歸調用它本身),而在控制返回後,原來的程序不會出現任何錯誤。

可重入代碼有一些共同特徵,例如不依賴存儲在堆上的公共系統資源、用到的狀態量都由參數中傳入、不調用非可重入的方法等。一個簡單的判斷方法:如果一個方法,他的返回結果是可預測的,只要輸入了相同的數據就能返回相同的結果,那他就滿足可重入性的要求。

線程本地存儲

如果能保證共享數據的代碼能在同一個線程中執行,這樣,無需同步也能保證線程之間不出現數據爭用的問題。

符合這中特點的應用並不少見,大部分使用消費隊列的架構模式(如“生產者-消費者”模式)都會講產品的消費過程儘量在一個線程中消費完,其中最重要的一個應用實例就是經典web交互模型中的“一個請求對應一個服務器線程”(Thread-per-Request)的處理方式,這種處理方式的廣泛應用使得很多web服務端應用都可以使用線程本地存儲來解決線程安全問題

java中一個變量要被某個線程獨享,可以通過java.lang.ThreadLocal類來實現線程本地存儲的功能。


鎖優化

1、自旋鎖與自適應鎖

自旋鎖

如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,我們就可以讓後面請求鎖的那個線程“稍等一下”,但不放棄處理器的執行時間,看看持有鎖的線程是否很快會釋放鎖。爲了讓線程等待,我們只需要讓線程執行一個忙循環(自旋)這項技術就是所謂的自旋鎖

自旋鎖在jdk 1.4.2中就已經引入,只不過默認是關閉的,可以使用 -XX:+UseSpinning參數來開啓,在jdk 1.6中已經改爲默認開啓了。

注意:自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應該使用傳統的方式去掛起線程了。自旋次數的默認值是10次,用戶可以使用參數 -XX:PreBlockSpin來更改。

自適應鎖

在jdk1.6中引入了自適應的自旋鎖。自適應意味着自旋的時間不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也很有可能再次成功,進而他將允許自旋等待持續相對更長的時間,比如100個循環。另外,如果對於某個鎖,自旋很少成功獲得過,那在以後要獲取這個鎖時將可能省略調自旋過程,以避免浪費處理器資源。

2、鎖消除

鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。

鎖消除的主要判斷依據來源於逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,那就可以把他們當做棧上數據對待,認爲他們是線程私有的,同步鎖自然就無需進行了。

3、鎖粗化

原則上,我們在編寫代碼的時候,總是推薦將同步塊的作用範圍限制的儘量小——只在共享數據的實際作用域中才進行同步,這樣是爲了使得需要同步的操作數量儘可能變小,如果存在鎖競爭,那等待的線程也能儘快拿到鎖。

4、輕量級鎖

輕量級鎖是jdk1.6之中加入的新型鎖機制,它名字中的“輕量級”是相對於使用操作系統互斥量來實現的傳統鎖而言的。首先要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。

圖片

圖片

5、偏向鎖

圖片

具體的解釋和理解可以參考:https://blog.csdn.net/zq1994520/article/details/84175573

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