1. 程式人生 > >Java中常用的鎖分析總結

Java中常用的鎖分析總結

Java中常用的鎖分析總結

1.    ReentrantLock、ReentrantReadWriteLock及Sychronized簡介

(a)  類繼承結構


ReentrantLock類繼承結構:


ReentrantReadWriteLick類繼承結構:

簡述:通過類的繼承結構可以看出ReentrantLock 和 ReentrantReadWriteLock是擁有者兩個不同類繼承結構的體系,兩者並無關聯。

Ps:Sychronized是一個關鍵字

(b) 幾個相關概念

什麼是可重入鎖:可重入鎖的概念是自己可以再次獲取自己的內部鎖。舉個例子,比如一條執行緒獲得了某個物件的鎖,此時這個物件鎖還沒有釋放,當其再次想要獲取這個物件的鎖的時候還是可以獲取的(如果不可重入的鎖的話,此刻會造成死鎖)。說的更高深一點可重入鎖是一種遞迴無阻塞的同步機制。

什麼叫讀寫鎖:讀寫鎖拆成讀鎖和寫鎖來理解。讀鎖可以共享,多個執行緒可以同時擁有讀鎖,但是寫鎖卻只能只有一個執行緒擁有,而且獲取寫鎖的時候其他執行緒都已經釋放了讀鎖,而且該執行緒獲取寫鎖之後,其他執行緒不能再獲取讀鎖。簡單的說就是寫鎖是排他鎖,讀鎖是共享鎖。

獲取鎖涉及到的兩個概念即 公平和非公平:公平表示執行緒獲取鎖的順序是按照執行緒加鎖的順序來分配的,即先來先得的FIFO順序。而非公平就是一種獲取鎖的搶佔機制,和公平相對就是先來不一定先得,這個方式可能造成某些執行緒飢餓(一直拿不到鎖)。

(c)  ReentrantLock,ReentrantReadWriteLock,Sychronized用法即作用

ReentrantLock: 類ReentrantLock實現了Lock,它擁有與Sychronized相同的併發性和記憶體語義,但是添加了類似鎖投票、定時鎖等候和可中斷等候的一些特性。此外,它還提供了在與激烈爭用情況下更佳的效能(說白了就是ReentrantLock和Sychronized差不多,執行緒間都是完全互斥的,一個時刻只能有一個執行緒獲取到鎖,執行被鎖住的程式碼,但ReentrantLock相對於Sychronized提供了更加豐富的功能並且線上程排程上做了優化,JVM排程使用ReentrantLock的執行緒會更快)

程式碼示例:ReentrantLockTest.java

/**

 * ReentrantLock DEMO

 * @author jianying.wcj

 * @date 2013-5-20

 */

public class ReetrantLockTest  {

       /**

        * 一個可重入鎖成員變數

        */

       private ReentrantLocklock =new ReentrantLock();

       public static void main(String[] args) {

              ReetrantLockTestdalt = new ReetrantLockTest();

           dalt.testLock();

       }

       public void testLock(){

              for(int i = 0; i < 5; i++) {

                     Threadthread = new Thread(new Runnable(){

                                          @Override

                                          publicvoid run() {

                                                 sayHello();

                                          }

                                   },"thread"+i);

                     thread.start();

              }

       }

       public void sayHello() {

              /**

               * 當一條執行緒不釋放鎖的時候,第二個執行緒走到這裡的時候就阻塞掉了

               */

              try {

              lock.lock();

                     System.out.println(Thread.currentThread().getName()+" locking ...");

                     System.out.println("Hello world!");

                     System.out.println(Thread.currentThread().getName()+" unlocking ...");

              }finally {

                  lock.unlock();

           }

    }

}

   執行結果:


簡述:首先要操作ReentrantLock的加鎖(lock)和解鎖(unlock)必須是針對同一個ReentrantLock物件,要是new 兩個ReetrantLock來分別完成對同一資源的加鎖和解鎖是沒有意義的。比如LockA物件對 resource 加鎖,讓後LockB物件對Resource解鎖,這個是不對的,沒有意義的)。通過執行結果可以看出,當一個執行緒去lock資源的時候,必須是上一個執行緒對資源完成了unlock,這個和syncronized關鍵字啟動的作用是一樣的。 另外在使用時一個需要格外主意的點是 unlock方法的呼叫要放在finally程式碼塊裡,來保證鎖一定會釋放,否則可能造成某一個資源一直被鎖死,排查問題比較困難。

ReentrantReadWriteLock:類ReentrantReadWriteLock實現了ReadWirteLock介面。它和ReentrantLock是不同的兩套實現,在類繼承結構上並無關聯。和ReentrantLock定義的互斥鎖不同的是,ReentrantReadWriteLock定義了兩把鎖即讀鎖和寫鎖。讀鎖可以共享,即同一個資源可以讓多個執行緒獲取讀鎖。這個和ReentrantLock(或者sychronized)相比大大提高了讀的效能。在需要對資源進行寫入的時候在會加寫鎖達到互斥的目的。話不多說看DEMO:

ReentrantReadWriteLock.java:

public class ReadWriteLockTest {

       /**

        * 一個可重入讀寫鎖

        */

       private ReentrantReadWriteLockreadWriteLock =new ReentrantReadWriteLock();

       /**

        * 讀鎖

        */

       private ReadLockreadLock =readWriteLock.readLock();

       /**

        * 寫鎖

        */

       private WriteLockwriteLock =readWriteLock.writeLock();

       /**

        * 共享資源

        */

       private StringshareData ="寂寞等待中...";

       public void write(String str) throws InterruptedException {

        writeLock.lock();

              System.err.println("ThreadName:"+Thread.currentThread().getName()+"locking...");

              try {

                     shareData = str;

                     System.err.println("ThreadName:" + Thread.currentThread().getName()+"修改為"+str);

                     Thread.sleep(1);

              }catch(InterruptedException e) {

                     e.printStackTrace();

              }finally {

                     System.err.println("ThreadName:" + Thread.currentThread().getName()+"  unlock...");

                     writeLock.unlock();

              }

       }

       public String read() {

              readLock.lock();

              System.out.println("ThreadName:" + Thread.currentThread().getName()+"lock...");

              try {

                     System.out.println("ThreadName:"+Thread.currentThread().getName()+"獲取為:"+shareData);

                     Thread.sleep(1);

              }catch(InterruptedException e) {

                     e.printStackTrace();

              }finally {

                     System.out.println("ThreadName:" + Thread.currentThread().getName()+"unlock...");

                     readLock.unlock();

              }

              returnshareData;

       }

       public static void main(String[] args) {

              final ReadWriteLockTest shareData =new ReadWriteLockTest();

              /**

               * 起50條讀執行緒

               */

              for(int i = 0; i < 50; i++) {

                     new Thread(new Runnable() {

                            publicvoid run() {

                                          try {

                                                 Thread.sleep(1);

                                          }catch (InterruptedException e) {

                                                 e.printStackTrace();

                                          }

                                          shareData.read();

                                   }

                     },"get Thread-read"+i).start();

              }

              for(int i = 0; i < 5; i++) {

                     new Thread(new Runnable() {

                            publicvoid run() {

                                   try {

                                          Thread.sleep(1);

                                   }catch (InterruptedException e1) {

                                          e1.printStackTrace();

                                   }

                                   try {

                                          shareData.write(new Random().nextLong()+"");

                                   }catch (InterruptedException e) {

                                          e.printStackTrace();

                                   }

                            }

                     },"wirte Thread-write"+i).start();

              }

       }

}

執行結果:



簡述:Demo讀鎖和寫鎖都是ReentrantReadWriteLock類定義的內部公開類,要想讓讀鎖和讀鎖或者讀鎖跟寫鎖產生共享或者互斥關係,必須要求讀鎖和寫鎖是有同一個ReentrantReadWriteLock產生的,否則是沒有意義的。從執行結果中可以看出讀鎖之間的共享,寫鎖和寫鎖,寫鎖和讀鎖之間的互斥關係。

Synchronized關鍵字:

public class SychronizedTest implements Runnable{

    public void run() { 

          synchronized(this) { 

               for (int i = 0; i < 5; i++) { 

                    System.out.println(Thread.currentThread().getName()+"synchronized loop " + i); 

               } 

          }

    }

    public static void main(String[] args) { 

            SychronizedTest t1 = new SychronizedTest(); 

          Thread ta = new Thread(t1,"A"); 

          Thread tb = new Thread(t1,"B"); 

          ta.start(); 

          tb.start(); 

    }

}

執行結果:


·

簡述:從執行記過來看,被sychronized包圍的程式碼是原子的。這個不多說,這個關鍵字大家應該都很熟悉。

2.    ReentrantLock、ReentrantReadWriteLock及Sychronized實現原理(原始碼級別)

(a)  鎖機制的內部實現

ReentrantLock內部鎖機制實現相關類圖:


簡述:ReentrantLock鎖機制的實現是基於它的一個成員變數sync,這個Sync是AbstractQueuedSynchronized(AQS)的一個子類(ps:sync類是ReentrantLock自己定義的一個內部類)。另外在ReentrantLock內部還定義了另外兩個類,分別是FairSync和NonFairSync,這兩個類就是分別對應的鎖公平分配和不公平分配的兩個實現,它們都繼承自Sync(類圖已經清晰的描述出來了繼承結構)。有關鎖的分配和釋放邏輯都是封裝在了AQS裡面的(AQS是AbstractQueuedSynchronized的簡稱,是JSR166規範中提出的一個基礎的同步中心類或者說是同步框架,其在內部實現了大量的同步操作,而且使用者還可以在此類的基礎上自定義自己的同步類),可見Sync和AQS是鎖機制實現的核心類(AQS詳述見下文)。

ReentrantLock當中的部分例項程式碼:

1.     兩個建構函式(可見預設使用的非公平鎖的分配機制):


2.     Lock方法的實現其實就是直接代理了Sync lock的實現:


3.     TryLock方法也是一樣的,都是代理自Sync


4.     解鎖方法


Ps:說白了ReentrantLock就是基於Sync的,而Sync就是一種AQS,其中核心機制AQS都實現好了。

               ReentrantReadWriteLock內部實現機制實現類圖:


          ReentrantReadWriteLock的類圖和ReentrantLock的類圖感覺是一摸一樣的,唯一的區別就是Sync、FairSync、NonSync是ReentrantReadWriteLock自己定義的。因為ReentrantReadWriteLock要實現讀寫鎖機制,所以這裡的Sync和ReentrantLock的Sync肯定不會相同。其他的和ReentrantLock都是一樣的,核心的實現都是基於AQS的子類Sync(AQS分析見下文)

              部分示例程式碼如下:

    1.建構函式(內部定義了ReadLock和WriteLock,預設也採用鎖非公平分配的實現)


  2. WriteLock當中的Lock方法:


   Ps:上文簡單的貼了兩行程式碼主要為了說明一點,ReentrantLock和ReentrantReadWriteLock的實現是基於AQS的。下文再從原始碼角度分析一下具體實現。

       Synchronized關鍵字:

       簡述:Synchronized實現的同步和上面提到的AQS的方式是不同的,AQS實現了一套自己的演算法來實現共享資源的合理控制(具體演算法實現,下文分析),而Synchronized實現的同步控制是基於java 內部的物件鎖的。

       Java內部物件鎖:JVM中每個物件和類實際上都與一把鎖與之相關聯,對於物件來說,監視的是這個物件變數,對於類來說,監視的是類變數。當虛擬機器裝載類時,會建立一個Class類的例項,鎖住的實際上是這個類對應的Class累的例項。物件鎖是可重入的,也就是說一個物件或者類上的鎖是可以累加的。

       Ps:java中的同步是通過監視器模型來實現的,Java中的監視器實際上是一個程式碼塊.

Synchronized實現分析:這麼說還是有點抽象,那麼從程式碼角度來分析一下Synchronized是怎麼實現的。

(a)   先看看Synchronized程式碼快的方式:

SynchronizedTest1.java:

package test9;

/**

 * @author jianying.wcj

 * @date 2013-5-22

 */

public classSynchronizedTest1 {

    public void sayHello(){

       synchronized(this){

           System.out.println("hello world!");

       }

    }

}

先用javac編譯成.class 然後再用javap–verbose SynchronizedTest1 檢視自己碼的彙編碼如下圖所示:


簡述:紅色標記出來的是兩條JVM命令,用來標識進入同步程式碼塊,和退出同步程式碼塊,由此可見Synchronized已經上升到JVM指令的級別和AQS的實現還是有很大差別的。上面這個是Synchronized程式碼塊的形式,Synchronized還有另一種使用方式就是同步方法。

(b)  Synchronized同步方法的方式:

SynchronizedTest2.java:

package test9;

/**

 * @author jianying.wcj

 * @date 2013-5-22

 */

public class SychronizedTest{

  public synchronized void sayHello(){

     System.out.println("hello world!");

  }

}

同樣通過javap命令檢視彙編碼如下:


簡述:通過看這段彙編碼,並沒有發現JVM的同步塊指令,可見同步方法和程式碼同步塊採用的是不同的實現方式。同步方法的實現是JVM定義了方法的訪問標誌 ACC_SYNCHRONIZED 在方法表中,JVM後將同步方法前面設定這個標誌,用於標識這個是一個同步方法。

3.    Sync及AQS的核心實現(原始碼級別)

AQS核心思想是,如果被請求的共享資源空閒,則將當前請求資源的執行緒設定為有效的工作執行緒,並且將共享資源的設定為鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套執行緒阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH佇列鎖實現的,即將暫時獲取不到鎖的執行緒加入到佇列中。

那麼首先看一下CLH佇列鎖的資料結構及實現演算法。

(a)CLH佇列的資料結構(如圖):


簡述:CLH佇列是一個虛擬的雙向佇列(虛擬的雙向佇列即不存在佇列例項,僅存在結點之間的關聯關係)。AQS是將每條請求共享資源的執行緒封裝成一個CLH鎖佇列的一個結點(Node)來實現鎖的分配的。具體構建佇列的演算法是這樣的:

假設: 有共享資源S目前正被L3執行緒佔用,此時有L1、L2執行緒分別對資源S進行lock操作以及獲取鎖後進行unlock操作。具體的流程如下:

(1)由於目前資源S被佔用,所以將執行緒L1包裝成一個CLH佇列的Node,將這個Node的前驅(prev)指向當前對列裡的隊尾,放入隊尾這個操作採用了CAS原語(原子操作)。如果當前的隊尾為NULL,那麼就建一個虛擬的Header,然後將T1執行緒掛載到虛擬Header下。核心程式碼如下:


Ps:  addWaiter就是放入佇列的操作。

 

Ps:採用CAS將節點加入到隊尾,如果隊尾為null進入enq操作。


Ps:建立了一個虛擬的Header

(2) L2執行緒請求資源S,那麼它和L1執行緒一樣將自己加入到隊尾,L2的prev指向L1,L1.next指向L2(雙向佇列嘛)。

(3) 當L3釋放資源即unlock的時候,喚醒與L3關聯的下一個節點,同時釋放當前節點。關鍵程式碼:

               

 (b)每個結點類的屬性及方法資訊:


屬性簡述:CANCELLED:表示因為超時或者中斷,結點被設定為取消狀態,被取消的狀態結點不應該去競爭鎖。SIGNAL:表示這個結點的繼任結點被阻塞了,因為等待某個條件而被阻塞。CONDITION:表示這個結點在佇列中,因為等待某個條件而被阻塞。這幾個是常量屬性預設值為:


這幾個常量用來設定waitStatus屬性。

Thread屬性表示關聯到這個結點的執行緒。Prev和next就是關聯前後結點的索引變數。NextWaiter 記錄的是這個結點是獨佔式還是可共享的屬性。

4.    幾種鎖的效能比較及使用場景(應用級別)

對於效能的對比這篇部落格介紹的比較好: