1. 程式人生 > >Java多執行緒程式設計中執行緒的同步與互斥/執行緒安全/Java鎖

Java多執行緒程式設計中執行緒的同步與互斥/執行緒安全/Java鎖

  • 摘要:多執行緒三個特徵:原子性、可見性以及有序性.>執行緒的同步與互斥?(同步執行緒與非同步執行緒,執行緒同步和非同步問題)  1.同步:假設現有執行緒A和執行緒B,執行緒A需要往緩衝區寫資料,執行緒B需要從緩衝區讀資料,但他們之間存在一種制約關係,即當執行緒A寫的時候,B不能來拿資料;B在拿資料的時候A不能往緩衝區寫,也就是說,只有當A寫完資料(或B取走資料),B才能來讀資料(或A才能往裡寫資料)。這種關係就是一種執行緒的同步關係。  同步關係則是多個執行緒彼此
  • 多執行緒三個特徵:原子性、可見性以及有序性.

    > 執行緒的同步與互斥?(同步執行緒與非同步執行緒,執行緒同步和非同步問題)

        1.同步:假設現有執行緒A和執行緒B,執行緒A需要往緩衝區寫資料,執行緒B需要從緩衝區讀資料,但他們之間存在一種制約關係,即當執行緒A寫的時候,B不能來拿資料;B在拿資料的時候A不能往緩衝區寫,也就是說,只有當A寫完資料(或B取走資料),B才能來讀資料(或A才能往裡寫資料)。這種關係就是一種執行緒的同步關係。

       同步關係則是多個執行緒彼此合作,通過一定的邏輯關係來共同完成一個任務。一般來說,同步關係中往往包含互斥,同時對臨界區的資源會按照某種邏輯順序進行訪問。如先生產後使用。

       同步並沒有通過指令monitorenter和monitorexit來完成(理論上其實也可以通過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其他任何執行緒都無法再獲得同一個monitor物件。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過位元組碼來完成。

        2.互斥:對於執行緒A和執行緒B來講,在同一時刻,只允許一個執行緒對臨界資源進行操作,即當A進入臨界區對資源操作時,B就必須等待;當A執行完,退出臨界區後,B才能對臨界資源進行操作。

      所謂互斥,就是不同執行緒通過競爭進入臨界區(共享的資料和硬體資源),為了防止訪問衝突,在有限的時間內只允許其中之一獨佔性的使用共享資源。如不允許同時寫;

       3.總的來說,兩者的區別就是:
    互斥是通過競爭對資源的獨佔使用,彼此之間不需要知道對方的存在,執行順序是一個亂序。

    同步是協調多個相互關聯執行緒合作完成任務,彼此之間知道對方存在,執行順序往往是有序的。

     

        互斥鎖的本質: 首先需要明確一點,互斥鎖實際上是一種變數,在使用互斥鎖時,實際上是對這個變數進行置0置1操作並進行判斷使得執行緒能夠獲得鎖或釋放鎖。  

 

    死鎖定義及產生的條件:  

死鎖:是由於兩個或者兩個以上的執行緒在對公共資源的爭奪時產生的一種阻塞狀態,如不借助外力的作用,執行緒很難繼續執行下去。
1、互斥屬性:即每次只能有一個執行緒佔用資源。  
2、請求與保持:即已經申請到鎖資源的執行緒可以繼續申請。在這種情況下,一個執行緒也可以產生死鎖情況,即抱著鎖找鎖。  
3、不可剝奪:執行緒已經得到所資源,在沒有自己主動釋放之前,不能被強行剝奪。  
4、迴圈等待:多個執行緒形成環路等待,每個執行緒都在等待相鄰執行緒的鎖資源。 
    死鎖的避免:  
1、既然死鎖的產生是由於使用了鎖,那麼在能不使用鎖的情況下就儘量不使用,如果有多種方案都能實現,那麼儘量不選用帶鎖的這種方案  

2、儘量避免同時獲得多把鎖,如果有必要,就要保證獲得鎖的順序相同。

> synchronized在靜態方法上表示呼叫前要獲得類的鎖,而在非靜態方法上表示呼叫此方法前要獲得物件的鎖。
public class StaticSynDemo {

 private static String a="test";
 public void print2(String b){
   synchronized (this) {//取得StaticSynDemo例項化後物件的鎖
    System.out.println(b+a);
   }
 }
  
 public static void print4(String b){
   synchronized (StaticSynDemo.class) { //取得StaticSynDemo.class類的鎖
    System.out.println(b+a);
   }
 }

}

  同步程式碼塊或同步物件:synchronized 方法和 synchronized 塊。

synchronized(this)、synchronized(*.class)與synchronized(任意物件)這幾種型別同步方法:

  同步synchronized(*.class)程式碼塊的作用其實和synchronized static方法作用一樣。Class鎖對類的所有物件例項起作用。

  this關鍵字代表類的一個物件,所以其記憶體鎖是針對相同物件的互斥操作,而static成員屬於類專有,其記憶體空間為該類所有成員共有,這就導致synchronized()對static成員加鎖,相當於對類加鎖,也就是在該類的所有成員間實現互斥,在同一時間只有一個執行緒可訪問該類的例項。

 

  Synchronized和Static Synchronized區別 
  一個是例項鎖(鎖在某一個例項物件上,如果該類是單例,那麼該鎖也具有全域性鎖的概念),一個是全域性鎖(該鎖針對的是類,無論例項多少個物件,那麼執行緒都共享該鎖)。 
  例項鎖對應的就是synchronized關鍵字,而類鎖(全域性鎖)對應的就是static synchronized(或者是鎖在該類的class或者classloader物件上)。
  synchronized(this)與synchronized(static XXX)的區別了,synchronized就是針對記憶體區塊申請記憶體鎖,this關鍵字代表類的一個物件,所以其記憶體鎖是針對相同物件的互斥操作,而static成員屬於類專有,其記憶體空間為該類所有成員共有,這就導致synchronized()對static成員加鎖,相當於對類加鎖,也就是在該類的所有成員間實現互斥,在同一時間只有一個執行緒可訪問該類的例項。

多執行緒三個特徵:原子性、可見性以及有序性。

 synchronized是通過同一時刻只有一個執行緒執行共享程式碼來保證多執行緒三個特徵的;

  volatile 變數具有 synchronized 的可見性特性,禁止指令重排,但是不具備原子特性。使用volatile變數,必須同時滿足下面兩個條件:

1.對變數的寫操作不依賴於當前值。  
2.該變數沒有包含在具有其他變數的不變式中。 

 當前常用的 多執行緒同步機制可以分為下面三種類型:  
1.volatile 變數:輕量級多執行緒同步機制,不會引起上下文切換和執行緒排程。僅提供記憶體可見性保證,不提供原子性。  
2.CAS 原子指令:輕量級多執行緒同步機制,不會引起上下文切換和執行緒排程。它同時提供記憶體可見性和原子化更新保證。  

3.內部鎖和顯式鎖:重量級多執行緒同步機制,可能會引起上下文切換和執行緒排程,它同時提供記憶體可見性和原子性。

   ---- java同步機制: volatile、synchronized和final.
  Java™ 語言包含兩種內在的同步機制:同步塊(或方法)和 volatile 變數。這兩種機制的提出都是為了實現程式碼執行緒的安全性。其中 Volatile 變數的同步性較差(但有時它更簡單並且開銷更低),而且其使用也更容易出錯。
  Java 語言中的 volatile 變數可以被看作是一種 “程度較輕的 synchronized”;與 synchronized 塊相比,volatile 變數所需的編碼較少,並且執行時開銷也較少,但是它所能實現的功能也僅是 synchronized 的一部分。
  鎖提供了兩種主要特性:互斥(mutual exclusion) 和可見性(visibility)。互斥即一次只允許一個執行緒持有某個特定的鎖,因此可使用該特性實現對共享資料的協調訪問協議,這樣,一次就只有一個執行緒能夠使用該共享資料。可見性要更加複雜一些,它必須確保釋放鎖之前對共享資料做出的更改對於隨後獲得該鎖的另一個執行緒是可見的 —— 如果沒有同步機制提供的這種可見性保證,執行緒看到的共享變數可能是修改前的值或不一致的值,這將引發許多嚴重問題。 

> 執行緒安全

 深入理解Java併發之synchronized實現原理- http://blog.csdn.net/javazejian/article/details/72828483

 -- 造成執行緒安全問題的主要誘因有兩點:

一是存在共享資料(也稱臨界資源),

二是存在多條執行緒共同操作共享資料。

 Java 虛擬機器中的同步(Synchronization)基於進入和退出管程(Monitor)物件實現, 無論是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步程式碼塊)還是隱式同步都是如此。在 Java 語言中,同步用的最多的地方可能是被 synchronized 修飾的同步方法。同步方法 並不是由 monitorenter 和 monitorexit 指令來實現同步的,而是由方法呼叫指令讀取執行時常量池中方法的 ACC_SYNCHRONIZED 標誌來隱式實現的.

      在單執行緒中不會出現執行緒安全問題,而在多執行緒程式設計中,有可能會出現同時訪問同一個資源的情況,這種資源可以是各種型別的的資源:一個變數、一個物件、一個檔案、一個數據庫表等,而當多個執行緒同時訪問同一個資源的時候,就會存在一個問題:
由於每個執行緒執行的過程是不可控的,所以很可能導致最終的結果與實際上的願望相違背或者直接導致程式出錯。執行緒安全問題,即多個執行緒同時訪問一個資源時,會導致程式執行結果並不是想看到的結果。
這裡面,這個資源被稱為:臨界資源(也有稱為共享資源)。
 也就是說,當多個執行緒同時訪問臨界資源(一個物件,物件中的屬性,一個檔案,一個數據庫等)時,就可能會產生執行緒安全問題。不過,當多個執行緒執行一個方法,方法內部的區域性變數並不是臨界資源,因為方法是在棧上執行的,而Java棧是執行緒私有的,因此不會產生執行緒安全問題。

    如何解決執行緒安全問題的呢?
基本上所有的併發模式在解決執行緒安全問題時,都採用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個執行緒訪問臨界資源,也稱作同步互斥訪問。通常來說,是在訪問臨界資源的程式碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其他執行緒繼續訪問。在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。
    在瞭解synchronized關鍵字的使用方法之前,我們先來看一個概念:互斥鎖,顧名思義:能到達到互斥訪問目的的鎖。舉個簡單的例子:如果對臨界資源加上互斥鎖,當一個執行緒在訪問該臨界資源時,其他執行緒便只能等待。
在Java中,每一個物件都擁有一個鎖標記(monitor),也稱為監視器,多執行緒同時訪問某個物件時,執行緒只有獲取了該物件的鎖才能訪問。
在Java中,可以使用synchronized關鍵字來標記一個方法或者程式碼塊,當某個執行緒呼叫該物件的synchronized方法或者訪問synchronized程式碼塊時,這個執行緒便獲得了該物件的鎖,其他執行緒暫時無法訪問這個方法,只有等待這個方法執行完畢或者程式碼塊執行完畢,這個執行緒才會釋放該物件的鎖,其他執行緒才能執行這個方法或者程式碼塊。

> Java鎖:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖

 synchronized屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層的作業系統的Mutex Lock來實現的,而作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的synchronized效率低的原因。慶幸的是在Java 6之後Java官方對從JVM層面對synchronized較大優化,所以現在的synchronized鎖效率也優化得很不錯了,Java 6之後,為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了輕量級鎖和偏向鎖,接下來我們將簡單瞭解一下Java官方在JVM層面對synchronized鎖的優化。 
  鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級. 讀寫鎖特點: 
1)多個讀者可以同時進行讀 
2)寫者必須互斥(只允許一個寫者寫,也不能讀者寫者同時進行) 
3)寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者) 

互斥鎖特點: 
  一次只能一個執行緒擁有互斥鎖,其他執行緒只有等待 
  
使用同步機制獲取 互斥鎖的情況,進行幾點說明: 
      1、如果同一個方法內同時有兩個或更多執行緒,則每個執行緒有自己的區域性變數拷貝。 
      2、類的每個例項都有自己的物件級別鎖。當一個執行緒訪問例項物件中的synchronized同步程式碼塊或同步方法時,該執行緒便獲取了該例項的物件級別鎖,其他執行緒這時如果要訪問synchronized同步程式碼塊或同步方法,便需要阻塞等待,直到前面的執行緒從同步程式碼塊或方法中退出,釋放掉了該物件級別鎖。 
      3、訪問同一個類的不同例項物件中的同步程式碼塊,不存在阻塞等待獲取物件鎖的問題,因為它們獲取的是各自例項的物件級別鎖,相互之間沒有影響。 
      4、持有一個物件級別鎖不會阻止該執行緒被交換出來,也不會阻塞其他執行緒訪問同一示例物件中的非synchronized程式碼。當一個執行緒A持有一個物件級別鎖(即進入了synchronized修飾的程式碼塊或方法中)時,執行緒也有可能被交換出去,此時執行緒B有可能獲取執行該物件中程式碼的時間,但它只能執行非同步程式碼(沒有用synchronized修飾),當執行到同步程式碼時,便會被阻塞,此時可能執行緒規劃器又讓A執行緒執行,A執行緒繼續持有物件級別鎖,當A執行緒退出同步程式碼時(即釋放了物件級別鎖),如果B執行緒此時再執行,便會獲得該物件級別鎖,從而執行synchronized中的程式碼。 
     5、持有物件級別鎖的執行緒會讓其他執行緒阻塞在所有的synchronized程式碼外。例如,在一個類中有三個synchronized方法a,b,c,當執行緒A正在執行一個例項物件M中的方法a時,它便獲得了該物件級別鎖,那麼其他的執行緒在執行同一例項物件(即物件M)中的程式碼時,便會在所有的synchronized方法處阻塞,即在方法a,b,c處都要被阻塞,等執行緒A釋放掉物件級別鎖時,其他的執行緒才可以去執行方法a,b或者c中的程式碼,從而獲得該物件級別鎖。 
     6、使用synchronized(obj)同步語句塊,可以獲取指定物件上的物件級別鎖。obj為物件的引用,如果獲取了obj物件上的物件級別鎖,在併發訪問obj物件時時,便會在其synchronized程式碼處阻塞等待,直到獲取到該obj物件的物件級別鎖。當obj為this時,便是獲取當前物件的物件級別鎖。 
    7、類級別鎖被特定類的所有示例共享,它用於控制對static成員變數以及static方法的併發訪問。具體用法與物件級別鎖相似。 
    8、互斥是實現同步的一種手段,臨界區、互斥量和訊號量都是主要的互斥實現方式。synchronized關鍵字經過編譯後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼指令。根據虛擬機器規範的要求,在執行monitorenter指令時,首先要嘗試獲取物件的鎖,如果獲得了鎖,把鎖的計數器加1,相應地,在執行monitorexit指令時會將鎖計數器減1,當計數器為0時,鎖便被釋放了。由於synchronized同步塊對同一個執行緒是可重入的,因此一個執行緒可以多次獲得同一個物件的互斥鎖,同樣,要釋放相應次數的該互斥鎖,才能最終釋放掉該鎖。 

  

無鎖、偏向鎖、輕量級鎖和重量級鎖:

在JDK 1.6中引入了“偏向鎖”和“輕量級鎖“。鎖一共有四種狀態:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖只能升級,不能降級。當對鎖的競爭加劇的時候,鎖會發生升級。 

1.偏向鎖 
之所以引入偏向鎖,是為了讓執行緒獲得鎖的代價更低。當一個執行緒訪問同步塊並獲取鎖的時候,會在物件的物件頭(物件頭包括兩部分的資訊:一部分是”Mark Word“,主要存放的是雜湊碼、物件的分代年齡、鎖的標記等資訊;另一部分是物件的型別指標)和棧幀中的鎖記錄中儲存鎖偏向的ID,以後該執行緒在進入方法的同步塊的時候,就檢查這個ID(可以理解為一種標記,是一種身份的標識),如果測試成功,表明物件已經獲得了鎖;如果測試失敗,繼續測試偏向鎖的標識是否設定為1(1的話就是偏向鎖),如果沒有則使用CAS(Compare And Swap)鎖。 

2.輕量級鎖 
分為加鎖和解鎖。當執行緒執行到同步塊之前,JVM會首先檢查當前執行緒的棧幀中建立用於儲存記錄鎖記錄的空間,並將物件頭中Mark Word複製到鎖記錄中,也稱為Displaced Mark Word,然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。如果成功,則執行緒獲得鎖,否則當前執行緒嘗試使用自旋來獲取鎖。這就是加鎖的過程。 

這裡多次提到CAS,那麼CAS是個什麼鬼?CAS是Compare and swap(比較和替換)的簡寫,具體而言就是:當進行CAS操作的時候,需要輸入兩個數值,一個是舊值,該舊值是原來的值,另一個是新值,也就是發生改變的值,得到這兩個值後,在CAS操作期間會去比較舊值是否發生變化,如果沒有發生變化就用新值進行替換,如果發生了變化就不進行替換。 

那麼解鎖的過程又是怎樣的呢?就是使用CAS操作將Displaced Mark Word替換回物件頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹,膨脹的結果是導致鎖的升級,並進入阻塞狀態。直到需要釋放鎖的執行緒釋放鎖並喚醒其他等待的執行緒。 

鎖的使用場景  
由於偏向鎖線上程存在競爭的時候會帶來額外的效能開銷,所以偏向鎖適用於只有一個執行緒方法同步快的情況;輕量級鎖線上程競爭鎖的情況下不會導致執行緒阻塞,但是會通過自旋消耗CPU,所以輕量級鎖適用於追求響應時間的情況。重量級鎖執行緒競爭不會使用自旋,但是執行緒競爭會導致阻塞,所以響應時間比較慢,重量級鎖一般使用在追求吞吐量的情況。 


    Java 語言包括了跨執行緒傳達併發性約束的構造 —— synchronized 和 volatile 。把程式碼塊宣告為 synchronized,有兩個重要後果,通常是指該程式碼具有 原子性(atomicity)和 可見性(visibility)。
    Condition是配合Lock使用的,而wait/notify是配合synchronized使用的。比較兩種實現方式,其實就是比較Lock和synchronized兩種同步機制的區別。 ReentrantLock 和 synchronized 的可伸縮性.