java線程安全和鎖優化

面向對象的編程思想是站在現實世界的角度去抽象和解決問題,他把數據和行爲都看作是對象的一部分,這樣可以讓程序員能以符合現實世界的思維方式來編寫和組織程序。
線程安全的一個恰當的定義:當多個線程訪問一個對象時,如果不用考慮這些線程在運行環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的 。
按照線程安全的安全程度由強至弱來排序,可以將java語言中各種操作共享的數據分爲以下5類:不可變、絕對線程安全
相對線程安全、線程兼容和線程對立。
(1)不可變:不可變的對象一定是線程安全的,無論是對象的方法實現還是方法的調用者,都不需要再採取任何線程安全的保障措施,
在java語言中,如果共享數據是一個基本數據類型,那麼只要再定義時使用final關鍵字修飾他就可以保證它是不可變的,如果共享數據是一個對象,那就需要保證對象的行爲不會對其狀態產生任何影響才行,就比如java.lang.string類的對象,她是一個典型的不可變的對象,我們調用他的substring()、replace()等方法都不會影響他原來的值,只會返回一個新構造的字符串對象。
(2)絕對線程安全:一個類要達到"不管運行時環境如何,調用者都不需要任何額外的同步措施"通常需要付出很大的,甚至有時候是不切實際的代價,在java  api中標註自己是線程安全的類,大多數都不是絕對的線程安全,比如java.util.vector是一個線程安全的容器,因爲它的add()、get()、size()等方法都是被synchronized修飾的,儘管這樣效率很低,但確實是安全的,但是在多線程的環境下,如果不在方法調用端做額外的同步措施的話,只用這段代碼仍然是不安全的,因爲如果另一個線程恰好在錯誤的時間裏刪除了一個元素,導致序號i已經不在可用的話,再用i訪問數組就會拋出異常,如果要保證這段代碼正常執行下去,就必須改變代碼。
(3)相對線程安全:相對線程安全就是我們所講的線程安全,它需要保證對這個對象單獨的操作是線程安全的,在調用的時候不需要做額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性,大部分線程安全類都屬於這種類型。
(4)線程兼容:線程兼容是指對象本身並不是線程安全的,但是可以通過在調用端正確的使用同步手段來保證對象在併發環境中可以安全的使用,
(5)線程對立:線程對立是指無論調用端是否採取了同步措施,都無法在多線程環境中併發使用的代碼,比如thread類的suspend()和resume()方法 ,如果兩個線程同時持有一個線程對象,一個嘗試去中斷線程,另一個嘗試去恢復線程,併發進行的話,無論調用時是否進行了同步,目標線程都是存在死鎖的風險。所以這兩個方法被jdk聲明廢棄了,
線程安全的實現方法:
(1)互斥同步(阻塞同步):同步是指在多個線程併發訪問共享數據時,保證共享數據在同一時刻只被一個線程使用,而互斥是實現同步的一種手段,
最基本的互斥同步手段就是synchronized關鍵字,synchronized關鍵字經過編譯之後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都需要一個reference類型的參數來指明要鎖定和解鎖的對象,如果java程序中synchronized明確指定了對象參數,那就是這個對象的reference,如果沒有明確指定,那就根據synchronized修飾的是實例方法還是類方法,去取對應的對象實例或class對象來作爲鎖對象,因爲同步塊在已進入的線程執行完之前,會阻塞後面其他線程的進入,java的線程是映射到操作系統的原生線程之上的,如果要阻塞或者喚醒一個線程,都需要操作系統來幫忙完成,這就需要從用戶態轉換到核心態,狀態轉換需要耗費很多的處理器時間,所以synchronized是java語言的一個重量級的操作。
還有實現同步的方法是java.util.concurrent包中的重入鎖(reentrantlock)來實現同步,他們的用法基本相似,都具備可重入性,區別是一個是api層面的互斥鎖,另一個是原生語法層面的互斥鎖,相比sychronized,reetrantlock增加了一些高級功能(等待可中斷、可實現公平鎖、鎖可以綁定多個條件)p392頁
(2)非阻塞同步:隨着硬件指令集的發展,我們有了另外一個選擇:基於衝突檢測的樂觀併發策略,也就是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了,如果共享數據有爭用,那就再採取其他補償措施,這個策略許多實現不需要把線程掛起,因此叫做非阻塞同步,
(3)無同步方案:要保證線程安全,並不一定就要進行同步,同步只是保證數據共享爭用時的正確性的手段,如果一個方法本身就不涉及數據共享 ,就不需要同步措施去保證正確性,兩類代碼天生就是安全的,可重入代碼和線程本地存儲。
鎖優化的措施:
(1)自旋鎖與自適應自旋:由於互斥同步對性能影響最大的是阻塞的實現,掛起線程和恢復線程的操作都需要轉入內核態中完成,因此如果物理機器上有一個以上的處理器,能讓兩個或以上的線程同時並行執行,我們就可以讓後面請求鎖的那個線程“稍等一下”,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖,爲了讓線程等待,我們只需要讓線程執行一個忙循環(自旋),這就是所謂的自旋鎖。
jdk1.4.2中就已經引入,不過默認是關閉的,在jdk1.6中就默認開啓了,自旋等待雖然本身避免了線程切換的開銷,但它要佔用處理器時間,如果鎖被佔用的時間很短,自旋等待的效果就非常好,如果被佔用時間過長,就會帶來性能上的浪費,因此自旋等待的時間必須有一定的限度,如果超過了限定的次數還沒有成功獲得鎖,就使用傳統的方式去掛起線程,
jdk1.6引入了自適應的自旋鎖,自適應意味着自旋的時間不固定,由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
(2)鎖消除(P398):鎖消除就是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除,鎖消除的判定依據來源於逃逸分析的數據支持,
(3)鎖粗化:大部分情況下,總是推薦將同步塊的作用範圍限制的儘量小,只在共享數據的實際作用域中財進行同步,這樣是爲了使得需要同步的操作數儘可能變小,但是如果一些列的操作都對同一個對象反覆加鎖和解鎖,則會導致不必要的性能損耗,例如:
public string concatstring(string s1,string s2,string s3){
    stringBuffer sb = new StringBuffer();
    sb.append(s1);
   sb.append(s2);
   sb.sppend(s3);
  return sb.tostring();
}
三個append操作都需要對同一個對象加鎖,所以會把加鎖同步的範圍擴展(粗化)到整個操作序列的外部。
(4)輕量級鎖:首先了解hotspot虛擬機的對象頭分爲兩部分,第一部分用於存儲對象自身的運行時數據,如哈希碼、分代年齡等,官方稱爲“mark word”,另一部分用於存儲指向方法區對象類型數據的指針
     加鎖的過程:在代碼進入同步塊的時候,如果此對象沒有被鎖定,虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄 (lock record)的空間,用於存儲鎖對象目前的mark word 的拷貝,然後虛擬機將使用CAS操作嘗試將對象的mark word更新爲lock record的指針,如果更新動作成功,則這個線程就擁有了該對象的鎖,並且對象mark word的鎖標誌位將轉變爲“00”,即表示此對象處於輕量級鎖定狀態,如果更新操作失敗,虛擬機首先會檢查對象的mark word是否指向當前線程的棧幀,如果只說明當前線程已經擁有了這個對象的鎖,那就可以直接進去同步塊繼續執行,否則說明這個鎖對象已經被其他線程搶佔了,如果兩個以上的線程爭用同一個鎖,則輕量級鎖就膨脹爲重量級,
(5)偏向鎖:目的是消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能,
          偏向鎖的意思是這個鎖會偏向於第一個獲得他的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程永遠不需要再進行同步,
       過程: 假設當前虛擬機啓用了偏向鎖,那麼當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲“01”,即偏向模式,同時使用CAS操作罷獲取到的鎖的線程的id記錄在獨享的mark  word中,如果cas操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作,當有另外一個線程嘗試去獲取這個鎖時,偏向模式就宣告結束,
    偏向鎖可以提高帶有同步但無競爭的程序性能,但是如果程序大多數的鎖總是被不同的線程訪問,那偏向模式就是多餘的 ,

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