1. 程式人生 > >Java中ReentrantLock的使用

Java中ReentrantLock的使用

一、基本概念和使用

可重入鎖: 也叫做遞迴鎖,指的是同一執行緒 外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受影響。

在JAVA中ReentrantLock 和synchronized 都是可重入鎖;

重入鎖ReentrantLock 相對來說是synchronized、Object.wait()和Object.notify()方法的替代品(或者說是增強版),在JDK5.0的早期版本,重入鎖的效能遠遠好於synchronized,但從JDK6.0開始,JDK在synchronized上做了大量的優化,使得兩者的效能差距並不大。但ReentrantLock也有一些synchronized沒法實現的特性。

ReentrantLock 在Java也是一個基礎的鎖,ReentrantLock 實現Lock介面提供一系列的基礎函式,開發人員可以靈活的是應用函式滿足各種複雜多變應用場景;

1.Lock介面:

Java中的ReentrantLock 也是實現了Java中鎖的核心介面Lock,在Lock介面定義了標準函式,但是具體實現是在實體類中[類似List和ArrayList、LinkedList關係];

   //獲取鎖,獲取不到lock就不罷休,不可被打斷,即使當前執行緒被中斷,執行緒也一直阻塞,直到拿到鎖, 比較無賴的做法。
    void lock();
	
   /**
   *獲取鎖,可中斷,如果獲取鎖之前當前執行緒被interrupt了,
   *獲取鎖之後會丟擲InterruptedException,並且停止當前執行緒;
   *優先響應中斷
   */
    void lockInterruptibly() throws InterruptedException;

	//立即返回結果;嘗試獲得鎖,如果獲得鎖立即返回ture,失敗立即返回false
    boolean tryLock();

	//嘗試拿鎖,可設定超時時間,超時返回false,即過時不候
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

   //釋放鎖
    void unlock();
	
	//返回當前執行緒的Condition ,可多次呼叫
    Condition newCondition();
2.ReentrantLock私有public方法

ReentrantLock中除了實現Lock中定義的一些標準函式外,同時提供其他的用於管理鎖的public方法:

	//傳入boolean值,true時create一個公平鎖,false為非公平鎖
    ReentrantLock(boolean fair) 
    
	//檢視有多少執行緒等待鎖
	int getQueueLength()
	
	//是否有執行緒等待搶鎖
	boolean hasQueuedThreads()
	
	//是否有指定執行緒等待搶鎖
	boolean hasQueuedThread(Thread thread)
	
	//當前執行緒是否搶到鎖。返回0代表沒有
	int getHoldCount()
	
	//查詢此鎖是否由任何執行緒持有
	 boolean isLocked()
	 
	 //是否為公平鎖
	boolean isFair() 
	
3.ReentrantLock中Condition的使用

ReentrantLock中另一個重要的應用就是Condition,Condition是Lock上的一個條件,可以多次newCondition()獲得多個條件,Condition可用於執行緒間通訊,通過Condition能夠更加精細的控制多執行緒的休眠與喚醒,而且在粒度和效能上都優於Object的通訊方法(wait、notify 和 notifyAll);
還時先看一下Condition 介面的原始碼吧:

public interface Condition {
	/**
	*Condition執行緒進入阻塞狀態,呼叫signal()或者signalAll()再次喚醒,
	*允許中斷如果在阻塞時鎖持有執行緒中斷,會丟擲異常;
	*重要一點是:在當前持有Lock的執行緒中,當外部呼叫會await()後,ReentrantLock就允許其他執行緒來搶奪鎖當前鎖,
	*注意:通過建立Condition物件來使執行緒wait,必須先執行lock.lock方法獲得鎖
	*/
    void await() throws InterruptedException;

    //Condition執行緒進入阻塞狀態,呼叫signal()或者signalAll()再次喚醒,不允許中斷,如果在阻塞時鎖持有執行緒中斷,繼續等待喚醒
    void awaitUninterruptibly();

    //設定阻塞時間,超時繼續,超時時間單位為納秒,其他同await();返回時間大於零,表示是被喚醒,等待時間並且可以作為等待時間期望值,小於零表示超時
    long awaitNanos(long nanosTimeout) throws InterruptedException;

	//類似awaitNanos(long nanosTimeout);返回值:被喚醒true,超時false
    boolean await(long time, TimeUnit unit) throws InterruptedException;

   //類似await(long time, TimeUnit unit) 
    boolean awaitUntil(Date deadline) throws InterruptedException;

   //喚醒指定執行緒
    void signal();
	
    //喚醒全部執行緒
    void signalAll();
}

ReentrantLock.Condition的執行緒通訊
ReentrantLock.Condition是在粒度效能上都優於Object的notify()、wait()、notifyAll()執行緒通訊的方式。

Condition中通訊方法相對Object的通訊在粒度上是粒度更細化,表現在一個Lock物件上引入多個Condition監視器、通訊方法中除了和Object對應的三個基本函式外,更是新增了執行緒中斷、阻塞超時的函式;
Condition中通訊方法相對Object的通訊在效能上更高效,效能的優化表現在ReentrantLock比較synchronized的優化 ;

基本示例程式碼:
多執行緒通訊/同步的一個經典的應用屬於**生產者消費者模式**,關於通過ReentrantLock的newCondition()是實現生產者消費者模式可以直接參考: 生產者消費者模式 ;

ReentrantLock.Condition執行緒通訊注意點:
  • 1.使用**ReentrantLock.Condition的signal()、await()、signalAll()方法使用之前必須要先進行lock()操作**[記得unlock()],類似使用Object的notify()、wait()、notifyAll()之前必須要對Object物件進行synchronized操作;否則就會拋IllegalMonitorStateException;
  • 2.注意在使用**ReentrantLock.Condition中使用signal()、await()、signalAll()方法,不能和Objectnotify()、wait()、notifyAll()方法混用,否則丟擲IllegalMonitorStateException`;
4.公平鎖與非公平鎖

公平鎖: 是指多個執行緒競爭同一資源時[等待同一個鎖時],獲取資源的順序是按照申請鎖的先後順序的;公平鎖保障了多執行緒下各執行緒獲取鎖的順序,先到的執行緒優先獲取鎖,有點像早年買火車票一樣排隊早的人先買到火車票;
基本特點: 執行緒執行會嚴格按照順序執行,等待鎖的執行緒不會餓死,但 整體效率相對比較低

非公平鎖: 是指多個執行緒競爭同一資源時,獲取資源的順序是不確定的,一般是搶佔式的;非公平鎖相對公平鎖是增加了獲取資源的不確定性,但是整體效率得以提升;
基本特點: 整體效率高,執行緒等待時間片具有不確定性

公平鎖與非公平鎖的測試demo:

重入鎖ReentrantLock實現公平鎖和非公平鎖很簡單的,因為ReentrantLock建構函式中可以直接傳入一個boolean值fair,對公平性進行設定。當fair為true時,表示此鎖是公平的,當fair為false時,表示此鎖是非公平的鎖;
來個簡單的demo;

 public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        ReentrantLock fairLock = new ReentrantLock(true);
        ReentrantLock unFairLock = new ReentrantLock();
        for (int i = 0; i < 10; i++) {
            threadPool.submit(new TestThread(fairLock,i," fairLock"));
            threadPool.submit(new TestThread(unFairLock, i, "unFairLock"));
        }
    }

    static class TestThread implements Runnable {
        Lock lock;
        int indext;
        String tag;

        public TestThread(Lock lock, int index, String tag) {
            this.lock = lock;
            this.indext = index;
            this.tag = tag;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getId() + " 執行緒 START  " + tag);
            meath();
        }

        private void meath() {
            lock.lock();
            try {
                if((indext&0x1)==1){
                    Thread.sleep(200);
                }else{
                    Thread.sleep(500);
                }
                System.out.println(Thread.currentThread().getId() + " 執行緒 獲得: Lock  ---》" + tag + "  Index:" + indext);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }

    }

二、ReentrantLock與synchronized簡單對比

ReentrantLock是JDK1.5之後引入的,synchronized作為關鍵字在ReentrantLock引入後進行的大量修改效能不斷提升;

1.可重入性

ReentrantLock和synchronized都具有可重入性,寫程式碼synchronized更簡單,ReentrantLock需要將lock()和unlock()進行一一對應否則有死鎖的風險;

2.鎖的實現方式

Synchronized作為Java關鍵字是依賴於JVM實現的,而ReenTrantLock是JDK實現的,有什麼區別,說白了就類似於作業系統來控制實現和使用者自己敲程式碼實現的區別。前者的實現是比較難見到的,後者有直接的原始碼可供閱讀。

3.公平性

ReentrantLock提供了公平鎖和非公平鎖兩種API,開發人員完全可以根據應用場景選擇鎖的公平性;
synchronized是作為Java關鍵字是依賴於JVM實現,Java團隊應該是優先考慮效能問題,因此synchronized是非公平鎖。

  • 小插曲
    之前看了很多博文有些人說synchronized是公平鎖有人說是非公平鎖,總之,看到讓人的苦笑不得,於是自己測試一下[JDK1.8]測試程式碼如下,結果很明顯synchronized就是一中非公平鎖。
    public synchronizedTest() {
        for(int i=0;i<20;i++){
            int finalI = i;
            new Thread(() ->
                test(finalI)
            ).start();
        }
    }
    synchronized  private void test(int index) {
            System.out.println("--------------- > Task :" + index);
    }
} 
4.二者效能和粒度

Java一直被詬病的就是效能問題,所以這是一個很重要的問題。
在Synchronized優化以前,synchronized的效能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,兩者的效能就差不多了,在兩種方法都可用的情況下,官方甚至建議使用synchronized,其實synchronized的優化我感覺就借鑑了ReenTrantLock中的CAS技術。都是試圖在使用者態就把加鎖問題解決,避免進入核心態的執行緒阻塞;

至於二者的細粒度差別就更明顯了,Synchronized只是關鍵字,而ReentrantLock則提供較為多樣的實現方式和更多的功能;

5.程式設計靈活度和難度

根據上面的介紹估計這個問題已近很明確了;
很明顯Synchronized的使用比較方便簡潔,並且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock需要手工宣告來加鎖和釋放鎖,為了避免忘記手工釋放鎖造成死鎖,所以最好在finally中宣告釋放鎖。其中ReenTrantLock使用不當死鎖問題更是讓人頭痛不已。
靈活度:很明顯ReentrantLock優於synchronized;
難度:也很明顯ReentrantLock難於synchronized;

  • ReenTrantLock實現的原理:
    在網上看到相關的原始碼分析,本來這塊應該是本文的核心,但是感覺比較複雜就不一一詳解了,簡單來說,ReenTrantLock的實現是一種自旋鎖,通過迴圈呼叫CAS操作來實現加鎖。它的效能比較好也是因為避免了使執行緒進入核心態的阻塞狀態。想盡辦法避免執行緒進入核心的阻塞狀態是我們去分析和理解鎖設計的關鍵鑰匙。
    什麼情況下使用ReenTrantLock:
    答案是,如果你需要實現ReenTrantLock的三個獨有功能時

三、ReentrantLock實際開發中的應用場景

1.公平鎖,執行緒排序執行,防餓死應用場景;

公平鎖原則必須按照鎖申請時間上先到先得的原則分配機制場景;

1).實現邏輯 上(包括:軟體中函式計算、業務先後流程;硬體中操作實現中順序邏輯)的順序排隊機制的場景;
軟體場景:使用者互動View中對使用者輸入結果分析類,分析過程後面演算法依賴上一步結果的場景,例如:推薦演算法實現[根據性別、年齡篩選]、阻塞佇列的實現
硬體場景:需要先分析確認使用者操作型別硬體版本或者廠家,然後發出操作指令;例如:自動售貨機;

2).現實 生活中 時間排序的 公平原則例如:客服分配,必須是先到先服務,不能出現餓死現象
公平鎖實現見上文:公平鎖與非公平鎖的測試demo:
邏輯程式碼實現那就沒法子實現了;
阻塞佇列的實現就是時間上的公平原則

示例程式碼:沒有!!!

2.非公平鎖,效率的體現者;

實際開發中最常用的的場景就是非公平鎖,ReentrantLock無參構造預設就時候非公平鎖;
適應場景除了上面公平鎖中提到的其他都是非公平鎖的使用場景;

示例程式碼:沒有!!!

3.ReentrantLock.Condition執行緒通訊

ReentrantLock.Condition執行緒通訊是最長見的面試題,這裡以最簡單例子:兩個執行緒之間交替列印 26英文字母和阿拉伯數字為demo:

private void alternateTask() {
        ReentrantLock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        Thread thread1 = new Thread(() -> {
            try {
                lock.lock();
                for (int i = 65; i < 91; i++) {
                    System.out.println("----------thread1------- " + (char) i);
                    condition2.signal();
                    condition1.await();
                }
                condition2.signal();
            } catch (Exception e) {
            } finally {
                lock.unlock();
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                lock.lock();
                for (int i = 0; i < 26; i++) {
                    System.out.println("----------thread2------- " + i);
                    condition1.signal();
                    condition2.await();
                }
                condition1.signal();
            } catch (Exception e) {
            } finally {
                lock.unlock();
            }
        });
        thread1.start();
        thread2.start();
    }
4.同步功能的使用

實現執行緒同步鎖synchronized 功能【單例為例】

  private Singleton() {
    }

    private static Singleton instance;
    private static Lock lock = new ReentrantLock();

    public static Singleton getInstance() {
        lock.lock();
        try {
            if (instance == null) {
                instance = new Singleton();
            }
        } finally {
            lock.unlock();
        }
        return instance;
    }
6.中斷殺器應用

ReentrantLock中lockInterruptibly()和lock()最大的區別就是中斷相應問題:
lock()是支援中斷相應的阻塞試的獲取方式,因此即使主動中斷了鎖的持有者,但是它不能立即unlock(),仍然要機械版執行完所有操作才會釋放鎖。
lockInterruptibly()是 優先響應中斷的,這樣有個優勢就是可以通過tryLock()、tryLock(timeout, TimeUnit.SECONDS)方法,中斷優先順序低的Task,及時釋放資源給優先順序更高的Task,甚至看到網上有人說可以做防止死鎖的優化;

例項程式碼:

 ReentrantLock lock = new ReentrantLock();
        try {
            lock.lockInterruptibly();
            if (lock.tryLock(timeout, TimeUnit.SECONDS)) {
                //TODO
            }else{
                //超時直接中斷優先順序低的Task
                Thread.currentThread().interrupt();
                lock.lock();
                //TODO
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
           
7.非重要任務Lock使用

優先順序較低的操作讓步給優先順序更高的操作,提示程式碼效率/使用者體驗;
忽略重複觸發
1).用在定時任務時,如果任務執行時間可能超過下次計劃執行時間,確保該有狀態任務只有一個正在執行,忽略重複觸發。
2).用在介面互動時點選執行較長時間請求操作時,防止多次點選導致後臺重複執行(忽略重複觸發)。
以上兩種情況多用於進行非重要任務防止重複執行,(如:清除無用臨時檔案,檢查某些資源的可用性,資料備份操作等)
tryLock()功能:如果已經獲得鎖立即返回fale,起到防止重複而忽略的效果

ReentrantLock lock = new ReentrantLock();
      //防止重複執行,執行耗時操作,例如使用者重複點選
       if (lock.tryLock()) {
           try {
			//TO DO
           } finally {
             lock.unlock();
           }
       }
      

超時放棄
定時操作的例如:錯誤日誌、定時過期快取清理的操作,遇到優先順序更高的操作佔用資源時,暫時放棄本次操作下次再處理,可以起到讓出CPU,提升使用者體驗;

  ReentrantLock lock = new ReentrantLock();
        try {
            if (lock.tryLock(timeout, TimeUnit.SECONDS)) {
                //TO DO
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

四、最後

1.本文最為學習ReentrantLock的學習筆記,使用文中應該有諸多紕漏錯誤之處,望諸位高手及時指正,菜雞感激不盡;
2.如一種所述本篇文章屬於學習筆記,所以其中有很多都是間了以下文章,在此一併感謝;