1. 程式人生 > >Java併發包中Lock的實現原理

Java併發包中Lock的實現原理

載自http://www.cnblogs.com/nullzx/p/4968674.html

1. Lock 的簡介及使用

         Lock是java 1.5中引入的執行緒同步工具,它主要用於多執行緒下共享資源的控制。本質上Lock僅僅是一個介面(位於原始碼包中的java\util\concurrent\locks中),它包含以下方法

//嘗試獲取鎖,獲取成功則返回,否則阻塞當前執行緒
void lock(); 

//嘗試獲取鎖,執行緒在成功獲取鎖之前被中斷,則放棄獲取鎖,丟擲異常 
void lockInterruptibly() throws InterruptedException; //嘗試獲取鎖,獲取鎖成功則返回true,否則返回false boolean tryLock(); //嘗試獲取鎖,若在規定時間內獲取到鎖,則返回true,否則返回false,未獲取鎖之前被中斷,則丟擲異常 boolean tryLock(long time, TimeUnit unit)
                                   throws InterruptedException; 

//釋放鎖
void unlock(); 

//返回當前鎖的條件變數,通過條件變數可以實現類似notify和wait的功能,一個鎖可以有多個條件變數
Condition newCondition();

        Lock有三個實現類,一個是ReentrantLock,另兩個是ReentrantReadWriteLock類中的兩個靜態內部類ReadLock和WriteLock。

          使用方法:多執行緒下訪問(互斥)共享資源時, 訪問前加鎖,訪問結束以後解鎖,解鎖的操作推薦放入finally塊中。

Lock l = ...; //根據不同的實現Lock介面類的建構函式得到一個鎖物件 
l.lock(); //獲取鎖位於try塊的外面 try { // access the resource protected by this lock } finally { l.unlock(); }

         注意,加鎖位於對資源訪問的try塊的外部,特別是使用lockInterruptibly方法加鎖時就必須要這樣做,這為了防止執行緒在獲取鎖時被中斷,這時就不必(也不能)釋放鎖。

try {
     l.lockInterruptibly();//獲取鎖失敗時不會執行finally塊中的unlock語句
      try{
          // access the resource protected by this lock
     }finally{
          l.unlock();
     }
} catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
}

2. 實現Lock介面的基本思想

          需要實現鎖的功能,兩個必備元素,一個是表示(鎖)狀態的變數(我們假設0表示沒有執行緒獲取鎖,1表示已有執行緒佔有鎖),另一個是佇列,佇列中的節點表示因未能獲取鎖而阻塞的執行緒。為了解決多核處理器下多執行緒快取不一致的問題,表示狀態的變數必須宣告為voaltile型別,並且對錶示狀態的變數和佇列的某些操作要保證原子性和可見性。原子性和可見性的操作主要通過Atomic包中的方法實現。

 

      執行緒獲取鎖的大致過程(這裡沒有考慮可重入和獲取鎖過程被中斷或超時的情況)

          1. 讀取表示鎖狀態的變數

         2. 如果表示狀態的變數的值為0,那麼當前執行緒嘗試將變數值設定為1(通過CAS操作完成),當多個執行緒同時將表示狀態的變數值由0設定成1時,僅一個執行緒能成功,其

            它執行緒都會失敗

            2.1 若成功,表示獲取了鎖,

                  2.1.1 如果該執行緒(或者說節點)已位於在佇列中,則將其出列(並將下一個節點則變成了佇列的頭節點)

                  2.1.2 如果該執行緒未入列,則不用對佇列進行維護

                  然後當前執行緒從lock方法中返回,對共享資源進行訪問。            

             2.2 若失敗,則當前執行緒將自身放入等待(鎖的)佇列中並阻塞自身,此時執行緒一直被阻塞在lock方法中,沒有從該方法中返回(被喚醒後仍然在lock方法中,並從下一條語句繼續執行,這裡又會回到第1步重新開始)

        3. 如果表示狀態的變數的值為1,那麼將當前執行緒放入等待佇列中,然後將自身阻塞(被喚醒後仍然在lock方法中,並從下一條語句繼續執行,這裡又會回到第1步重新開始)

          注意: 喚醒並不表示執行緒能立刻執行,而是表示執行緒處於就緒狀態,僅僅是可以執行而已

 

      執行緒釋放鎖的大致過程

        1. 釋放鎖的執行緒將狀態變數的值從1設定為0,並喚醒等待(鎖)佇列中的隊首節點,釋放鎖的執行緒從就從unlock方法中返回,繼續執行執行緒後面的程式碼

        2. 被喚醒的執行緒(佇列中的隊首節點)和可能和未進入佇列並且準備獲取的執行緒競爭獲取鎖,重複獲取鎖的過程

        注意:可能有多個執行緒同時競爭去獲取鎖,但是一次只能有一個執行緒去釋放鎖,佇列中的節點都需要它的前一個節點將其喚醒,例如有佇列A<-B-<C ,即由A釋放鎖時喚醒B,B釋放鎖時喚醒C

 

3. 公平鎖和非公平鎖

         鎖可以分為公平鎖和不公平鎖,重入鎖和非重入鎖(關於重入鎖的介紹會在ReentrantLock原始碼分析中介紹),以上過程實際上是非公平鎖的獲取和釋放過程。

公平鎖嚴格按照先來後到的順去獲取鎖,而非公平鎖允許插隊獲取鎖。

          公平鎖獲取鎖的過程上有些不同,在使用公平鎖時,某執行緒想要獲取鎖,不僅需要判斷當前表示狀態的變數的值是否為0,還要判斷佇列裡是否還有其他執行緒,若佇列中還有執行緒則說明當前執行緒需要排隊,進行入列操作,並將自身阻塞;若佇列為空,才能嘗試去獲取鎖。而對於非公平鎖,當表示狀態的變數的值是為0,就可以嘗試獲取鎖,不必理會佇列是否為空,這樣就實現了插隊獲取鎖的特點。通常來說非公平鎖的吞吐率比公平鎖要高,我們一般常用非公平鎖。

           這裡需要解釋一點,什麼情況下才會出現,表示鎖的狀態的變數的值是為0而且佇列中仍有其它執行緒等待獲取鎖的情況。

           假設有三個執行緒A、B、C。A執行緒為正在執行的執行緒並持有鎖,佇列中有一個C執行緒,位於隊首。現在A執行緒要釋放鎖,具體執行的過程操作可分為兩步:

            1. 將表示鎖狀態的變數值由1變為0,

            2. C執行緒被喚醒,這裡要明確兩點:

              (1)C執行緒被喚醒並不代表C執行緒開始執行,C執行緒此時是處於就緒狀態,要等待作業系統的排程

              (2)C執行緒目前還並未出列,C執行緒要進入執行狀態,並且通過競爭獲取到鎖以後才會出列。

            如果C執行緒此時還沒有進入執行態,同時未在佇列中的B執行緒進行獲取鎖的操作,B就會發現雖然當前沒有執行緒持有鎖,但是佇列不為空(C執行緒仍然位於佇列中),要滿足先來後到的特點(B在C之後執行獲取鎖的操作),B執行緒就不能去嘗試獲取鎖,而是進行入列操作。

 

4. 實現Condition介面的基本思想

         Condition 本質是一個介面,它包含如下方法

// 讓執行緒進入等通知待狀態 
void await() throws InterruptedException; 
void awaitUninterruptibly();
 
//讓執行緒進入等待通知狀態,超時結束等待狀態,並丟擲異常  
long awaitNanos(long nanosTimeout) throws InterruptedException; 
boolean await(long time, TimeUnit unit) throws InterruptedException; 
boolean awaitUntil(Date deadline) throws InterruptedException; 

//將條件佇列中的一個執行緒,從等待通知狀態轉換為等待鎖狀態 
void signal(); 

//將條件佇列中的所有執行緒,從等待通知阻塞狀態轉換為等待鎖阻塞狀態
void signalAll();

           一個Condition例項的內部實際上維護了一個佇列,佇列中的節點表示由於(某些條件不滿足而)執行緒自身呼叫await方法阻塞的執行緒。Condition介面中有兩個重要的方法,即 await方法和 signal方法。執行緒呼叫這個方法之前該執行緒必須已經獲取了Condition例項所依附的鎖。這樣的原因有兩個,(1)對於await方法,它內部會執行釋放鎖的操作,所以使用前必須獲取鎖。(2)對於signal方法,是為了避免多個執行緒同時呼叫同一個Condition例項的singal方法時引起的(佇列)出列競爭。下面是這兩個方法的執行流程。

          await方法:

                            1. 入列到條件佇列(注意這裡不是等待鎖的佇列

                            2. 釋放鎖

                            3. 阻塞自身執行緒

                             ------------被喚醒後執行-------------

                            4. 嘗試去獲取鎖(執行到這裡時執行緒已不在條件佇列中,而是位於等待(鎖的)佇列中,參見signal方法)

                                4.1 成功,從await方法中返回,執行執行緒後面的程式碼

                                4.2 失敗,阻塞自己(等待前一個節點釋放鎖時將它喚醒)

         注意:await方法時自身執行緒呼叫的,執行緒在await方法中阻塞,並沒有從await方法中返回,當喚醒後繼續執行await方法中後面的程式碼(也就是獲取鎖的程式碼)。可以看出await方法釋放了鎖,又嘗試獲得鎖。當獲取鎖不成功的時候當前執行緒仍然會阻塞到await方法中,等待前一個節點釋放鎖後再將其喚醒。

 

         signal方法:

                           1. 將條件佇列的隊首節點取出,放入等待鎖佇列的隊尾

                           2. 喚醒該節點對應的執行緒

         注意:signal是由其它執行緒呼叫

condition

Lock和Condition的使用例程

           下面這個例子,就是利用lock和condition實現B執行緒先列印一句資訊後,然後A執行緒列印兩句資訊(不能中斷),交替十次後結束

public class ConditionDemo {
    volatile int key = 0;
    Lock l = new ReentrantLock();
    Condition c = l.newCondition();
    
    public static  void main(String[] args){
        ConditionDemo demo = new ConditionDemo();
        new Thread(demo.new A()).start();
        new Thread(demo.new B()).start();
    }
    
    class A implements Runnable{
        @Override
        public void run() {
            int i = 10;
            while(i > 0){
                l.lock();
                try{
                    if(key == 1){
                        System.out.println("A is Running");
                        System.out.println("A is Running");
                        i--;
                        key = 0;
                        c.signal();
                    }else{
                     c.awaitUninterruptibly();                        
                    }
                    
                }
                finally{
                    l.unlock();
                }
            }
        }
        
    }
    
    class B implements Runnable{
        @Override
        public void run() {
            int i = 10;
            while(i > 0){
                l.lock();
                try{
                    if(key == 0){
                        System.out.println("B is Running");
                        i--;
                        key = 1;
                        c.signal();
                    }else{
                     c.awaitUninterruptibly();                        
                    }
                    
                }
                finally{
                    l.unlock();
                }
            }
        }    
    }
}

5. Lock與synchronized的區別

          1. Lock的加鎖和解鎖都是由java程式碼配合native方法(呼叫作業系統的相關方法)實現的,而synchronize的加鎖和解鎖的過程是由JVM管理的

          2. 當一個執行緒使用synchronize獲取鎖時,若鎖被其他執行緒佔用著,那麼當前只能被阻塞,直到成功獲取鎖。而Lock則提供超時鎖和可中斷等更加靈活的方式,在未能獲取鎖的     條件下提供一種退出的機制。

          3. 一個鎖內部可以有多個Condition例項,即有多路條件佇列,而synchronize只有一路條件佇列;同樣Condition也提供靈活的阻塞方式,在未獲得通知之前可以通過中斷執行緒以    及設定等待時限等方式退出條件佇列。

         4. synchronize對執行緒的同步僅提供獨佔模式,而Lock即可以提供獨佔模式,也可以提供共享模式