1. 程式人生 > 實用技巧 >鎖機制

鎖機制

在java中的鎖分為以下(其實就是按照鎖的特性和設計來劃分

1、公平鎖/非公平鎖

2、可重入鎖

3、獨享鎖/共享鎖

4、互斥鎖/讀寫鎖

5、樂觀鎖/悲觀鎖

6、分段鎖

7、偏向鎖/輕量級鎖/重量級鎖

8、自旋鎖(java.util.concurrent包下的幾乎都是利用鎖)

從底層角度看常見的鎖也就兩種:Synchronized和Lock介面以及ReadWriteLock介面(讀寫鎖)

從類關係看出Lock介面是jdk5後新添的來實現鎖的功能,其實現類:ReentrantLock、WriteLock、ReadLock。

其實還有一個介面ReadWriteLock,讀寫鎖(讀讀共享、讀寫獨享、寫讀獨享、寫寫獨享)。

Lock介面與synchronized關鍵字本質上都是實現同步功能。

區別:ReentrantLock:使用上需要顯示的獲取鎖和釋放鎖,提高可操作性、可中斷的獲取獲取鎖以及可超時的獲取鎖,預設是 非公平的但可以實現公平鎖,悲觀,獨享,互斥,可重入,重量級鎖。

ReentrantReadWriteLock:預設非公平但可實現公平的,悲觀,寫獨享,讀共享,讀寫,可重入,重量級鎖。

synchronized:關鍵字,隱式的獲取鎖和釋放鎖,不具備可中斷、可超時,非公平、互斥、悲觀、獨享、可重入的重量級Lock的使用也很簡單:

Lock lock = new
ReentrantLock(); lock.lock(); try{ }finally{ lock.unlock(); } //注意:不要將lock方法寫在try塊中,因為如果在獲取鎖的時候發生異常,異常丟擲的同時也會導致鎖無故的釋 //放 否則會程式會報監視狀態異常 Exception in thread "執行緒一" java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:
1260) at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460) //ReentrantLock必須要在finally中unlock(), 否則,如果在被加鎖的程式碼中丟擲了異常,那麼這個鎖將會永遠無法釋放. //synchronized就沒有這樣的問題, 遇到異常退出時,會釋放掉已經獲得的鎖。

Lock介面提供的 ,synchronized關鍵字所不具備的特性

以下測試程式碼,測試

Lock lock = new ReentrantLock();
         final MMT m = new MMT(lock);
         Thread tt = new Thread(new Runnable() {
             @Override
             public void run() {
                 System.out.println("執行緒一 開始執行。。。");
                 try {
                    m.update("張三");
                } catch (InterruptedException e) {
                           System.out.println(Thread.currentThread().getName()+"被中斷(鎖釋放)。。。");
                }
                 System.out.println("執行緒一 結束執行。。。");
             }
         },"執行緒一");
         
         Thread tt2 = new Thread(new Runnable() {
             @Override
             public void run() {
                 
                 System.out.println("執行緒二 開始執行。。。");
                 try {
                    m.update("李四");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    System.out.println(Thread.currentThread().getName()+"被中斷(鎖釋放)。。。");
                }
                 System.out.println("執行緒二 結束執行。。。");
             }
         },"執行緒二");
         
         tt.start();
         tt2.start();
         //中斷執行緒
         tt.interrupt();
         try {
            tt.join();
            tt2.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
         
        
    }
 
class MMT {
    String name;
    Lock lock=null;
    public MMT(Lock lock) {
        this.lock=lock;
    }
   public void update(String name) throws InterruptedException{
//       lock.lock();
//       boolean tryLock = lock.tryLock();//嘗試獲取鎖
       //中斷只是在當前執行緒獲取鎖之前,或者當前執行緒獲取鎖的時候被阻塞
//       lock.lockInterruptibly();
       lock.tryLock(3000, TimeUnit.SECONDS);
       try{
           setName(name);
           System.out.println(Thread.currentThread().getName()+" 變換後的姓名為"+name);
       }finally{
           lock.unlock();
       }
   }
   
   public void setName(String name) {
    this.name = name;
   }
   public String getName() {
    return name;
   }
}

可實現公平鎖

對於ReentrantLock而言,可實現公平鎖 ,通過建構函式指定是否需要公平,預設是非公平,區別在與非公平隨機性,並且高併發下吞吐量大,公平的話根據請求鎖等待的時間長短,等待的長了優先,類似FIFO,吞吐量降低了。

鎖繫結多個條件

指ReentrantLock物件可以同時繫結多個Condition條件物件,而在Synchroized中,鎖物件的wait方法、notify方法、和notifyall方法可以實現一個隱含條件,如果需要多個,得額外的新增一個鎖物件。在ReentrantLock中不需要,只需要建立多個條件物件即可(new Condition()),對應的await()、siganl()、signalAll()。

synchronized的優勢

synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在程式碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過程式碼實現的,要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中

應用場景:

在資源競爭不激烈的情況下,synchronized關鍵字的效能優與ReentrantLock,相反,ReentrantLock的效能保持常態,優於關鍵字。

按照其性質劃分:

公平鎖/非公平鎖

公平鎖指多個執行緒按照申請鎖的順序來依次獲取鎖。非公平鎖指多個執行緒獲取鎖的順序並不是按照申請鎖的順序來獲取,有可能後申請鎖的執行緒比先申請鎖的執行緒優先獲取到鎖,此極大的可能會造成執行緒飢餓現象,遲遲獲取不到鎖。由於ReentrantLock是通過AQS來實現執行緒排程,可以實現公平鎖,,但是synchroized是非公平的,無法實現公平鎖。

/**
 * 公平鎖與非公平鎖測試
 */
public class FairAndUnFairThreadT {
 
 
    public static void main(String[] args) throws InterruptedException {
        //預設非公平鎖
        final Lock lock = new ReentrantLock(true);
        final MM m = new MM(lock);
        for (int i=1;i<=20 ;i++){
            String name = "執行緒"+i;
            Thread tt = new Thread(new Runnable() {
                @Override
                public void run() {
                   for(int i=0;i<2;i++){
                       m.testReentrant();
                   }
                }
            },name);
            tt.start();
        }
 
    }
}
class MM {
    Lock lock = null;
    MM(Lock lock){
        this.lock = lock;
    }
 
    public void testReentrant(){
        lock.lock();
        try{
            Thread.sleep(1);
            System.out.println(Thread.currentThread().getName() );
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
 
    public synchronized  void testSync(){
        System.out.println(Thread.currentThread().getName());
    }
 
}

但是未必絕對就是按照順序,可能因為CPU準備原因,可能個別會不是公平的。

樂觀鎖與悲觀鎖

不是指什麼具體型別的鎖,而是指在併發同步的角度。悲觀鎖認為對於共享資源的併發操作,一定是發生xi修改的,哪怕沒有發生修改,也會認為是修改的,因此對於共享資源的操作,悲觀鎖採取加鎖的方式,認為,不加鎖的併發操作一定會出現問題。樂觀鎖認為對於共享資源的併發操作是不會發生修改的,在更新資料的時候,會採用嘗試更新,不斷重試的方式更新資料。樂觀的認為,不加鎖的併發操作共享資源是沒問題的。從上面的描述看除,樂觀鎖不加鎖的併發操作會帶來效能上的提升,悲觀鎖的使用就是利用synchroized關鍵字或者lock介面的特性。樂觀鎖在java中的使用,是無鎖程式設計常常採用的是CAS自旋鎖,典型的例子就是併發原子類,通過CAS自旋(spinLock)來更新值。

獨享鎖與共享鎖

獨享鎖是指該鎖一次只能被一個執行緒所持有。共享鎖是指可被多個執行緒所持有。在java中,對ReentrantLock物件以及synchroized關鍵字而言,是獨享鎖的。但是對於ReadWriteLock介面而言,其讀是共享鎖,其寫操作是獨享鎖。讀鎖的共享鎖是可保證併發讀的效率,讀寫、寫寫、寫讀的過程中都是互斥的,獨享的。獨享鎖與共享鎖在Lock的實現中是通過 AQS(抽象佇列同步器)來實現的。

互斥鎖與讀寫鎖

互斥鎖與讀寫鎖就是具體的實現,互斥鎖在java 中的體現就是synchronized關鍵字以及Lock介面實現類ReentrantLock,讀寫鎖在java中的具體實現就是ReentrantReadWriteLock。

可重入鎖

又名遞迴鎖,是指同一個執行緒在外層的方法獲取到了鎖,在進入內層方法會自動獲取到鎖。對於ReentrantLock和synchronized關鍵字都是可重入鎖的。最大的好處就是能夠避免一定程度的死鎖。

public sychrnozied void test() {
    //執行邏輯,呼叫另一個加鎖的方法
    test2();
}
 
public sychronized void test2() {
    //執行業務邏輯
}

在上面程式碼中,sychronized關鍵字加在類方法上,執行test方法獲取當前物件作為監視器的物件鎖,然後又呼叫test2同步方法。

一、如果鎖是可重入的話,那麼當前執行緒就在呼叫test2時並不需要再次獲取當前鎖物件,可以直接進入test2方法。

二、如果鎖是不具備可重入的話,那麼該執行緒在呼叫test2前會等待當前物件鎖的釋放,實際上該物件鎖已被當前執行緒所持有不可能再此獲得。那麼就會發生死鎖。

按照設計方案來分類(目的對鎖的進一步優化)

自旋鎖與自適應自旋鎖(或者說是自旋鎖的變種TicketLock、MCSLock、CLHLock)

底層採用CAS來保證原子性,自旋鎖獲取鎖的時候不會阻塞,而是通過不斷的while迴圈的方式嘗試獲取鎖。優點:減少執行緒上下文切換的消耗,缺點是會消耗CPU。如果鎖被佔用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被佔用的時間很長,那麼自旋的執行緒只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來效能上的浪費。

偏向鎖、輕量級鎖、重量級鎖

這三種鎖是指鎖的狀態,並且是針對Synchronized,在java通過引入鎖升級的機制來實現高校的synchronized。鎖的狀態是通過物件監視器在物件頭中的欄位來表明的。

偏向鎖:指一段同步程式碼一直被同一個執行緒s所訪問,那麼該執行緒會自動的獲取鎖。降低獲取鎖的代價。

輕量級鎖:當鎖是偏向鎖的時候,被另一個執行緒所訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,提高效能。

重量級鎖:當鎖為輕量級鎖的時候,另一個執行緒雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒獲取到鎖就會進入阻塞,該鎖膨脹為重量級鎖。重量級會讓其他申請執行緒阻塞,效能降低。

偏向所鎖,輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。一個物件剛開始例項化的時候,沒有任何執行緒來訪問它的時候。它是可偏向的,意味著,它現在認為只可能有一個執行緒來訪問它,所以當第一個執行緒來訪問它的時候,它會偏向這個執行緒,此時,物件持有偏向鎖。偏向第一個執行緒,這個執行緒在修改物件頭成為偏向鎖的時候使用CAS操作,並將物件頭中的ThreadID改成自己的ID,之後再次訪問這個物件時,只需要對比ID,不需要再使用CAS在進行操作。

一旦有第二個執行緒訪問這個物件,因為偏向鎖不會主動釋放,所以第二個執行緒可以看到物件時偏向狀態,這時表明在這個物件上已經存在競爭了,檢查原來持有該物件鎖的執行緒是否依然存活,如果掛了,則可以將物件變為無鎖狀態,然後重新偏向新的執行緒,如果原來的執行緒依然存活,則馬上執行那個執行緒的操作棧,檢查該物件的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級為輕量級鎖,(偏向鎖就是這個時候升級為輕量級鎖的)。如果不存在使用了,則可以將物件回覆成無鎖狀態,然後重新偏向。
輕量級鎖認為競爭存在,但是競爭的程度很輕,一般兩個執行緒對於同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個執行緒就會釋放鎖。 但是當自旋超過一定的次數,或者一個執行緒在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖,重量級鎖使除了擁有鎖的執行緒以外的執行緒都阻塞,防止CPU空轉。

分段鎖

分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱為Segment,它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry陣列,陣列中的每個元素又是一個連結串列;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。

當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然後對這個分段進行加鎖,所以當多執行緒put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。

但是,在統計size的時候,可就是獲取hashmap全域性資訊的時候,就需要獲取所有的分段鎖才能統計。分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個陣列的時候,就僅僅針對陣列中的一項進行加鎖操作。