1. 程式人生 > 實用技巧 >併發程式設計(八)Lock鎖

併發程式設計(八)Lock鎖

一、引言

  執行緒併發的過程中,肯定會設計到一個變數共享的概念,那麼我們在多執行緒執行過程中,怎麼保證每個先拿獲取的變數資訊都是最新且有序的呢?這一篇我們來專門學習一下Lock鎖。

  我們先來了解幾個概念:

樂觀鎖與悲觀鎖

悲觀鎖:

  假定會發生併發衝突,即共享資源會被某個執行緒更改。所以當某個執行緒獲取共享資源時,會阻止別的執行緒獲取共享資源。也稱獨佔鎖或者互斥鎖,例如java中的synchronized同步鎖。

樂觀鎖:

  假設不會發生併發衝突,只有在最後更新共享資源的時候會判斷一下在此期間有沒有別的執行緒修改了這個共享資源。如果發生衝突就重試,直到沒有衝突,更新成功。CAS就是一種樂觀鎖實現方式。

PS:CAS相關知識戳這裡~

公平鎖與非公平鎖

  • 公平鎖的實現就是誰等待時間最長,誰就先獲取鎖
  • 非公平鎖就是隨機獲取的過程,誰運氣好,cpu時間片輪詢到哪個執行緒,哪個執行緒就能獲取鎖

可重入鎖與不可重入鎖

不可重入鎖

  若當前執行緒執行中已經獲取了鎖,如果再次獲取該鎖時,就會獲取不到被阻塞。

可重入鎖

  每一個鎖關聯一個執行緒持有者計數器,當計數器為 0 時表示該鎖沒有被任何執行緒持有,那麼任何執行緒都可能獲得該鎖而呼叫相應的方法;當某一執行緒請求成功後,JVM會記下鎖的持有執行緒,並且將計數器置為 1;此時其它執行緒請求該鎖,則必須等待;而該持有鎖的執行緒如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增;當執行緒退出同步程式碼塊時,計數器會遞減,如果計數器為 0,則釋放該鎖。

二、Condition

  在使用Lock之前,我們使用的最多的同步方式應該是synchronized關鍵字來實現同步方式了。配合Object的wait()notify()系列方法可以實現執行緒的等待/通知模式

  PS:Condition的實質是通過控制執行緒的等待和喚醒來達到控制指定執行緒的功能。

  特點

  • 依賴於Lock物件,呼叫Lock物件的newCondition()物件建立而來
  • 可以實現等待/通知形式的執行緒互動模式
  • 可以有選擇性的進行執行緒通知,喚醒指定執行緒

基本方法

public interface Condition {
    void await() throws
InterruptedException; boolean await(long var1, TimeUnit var3) throws InterruptedException; long awaitNanos(long var1) throws InterruptedException; void awaitUninterruptibly(); boolean awaitUntil(Date var1) throws InterruptedException; void signal(); void signalAll(); }
  • await() :造成當前執行緒在接到訊號或被中斷之前一直處於等待狀態。
  • await(long time, TimeUnit unit) :造成當前執行緒在接到訊號、被中斷到達指定等待時間之前一直處於等待狀態。
  • awaitNanos(long nanosTimeout) :造成當前執行緒在接到訊號、被中斷到達指定等待時間之前一直處於等待狀態。返回值表示剩餘時間,如果在nanosTimesout之前喚醒,那麼返回值 = nanosTimeout - 消耗時間,如果返回值 <= 0 ,則可以認定它已經超時了。
  • awaitUninterruptibly() :造成當前執行緒在接到訊號之前一直處於等待狀態。【注意:該方法對中斷不敏感】。
  • awaitUntil(Date deadline) :造成當前執行緒在接到訊號、被中斷或到達指定最後期限之前一直處於等待狀態。如果沒有到指定時間就被通知,則返回true,否則表示到了指定時間,返回返回false。
  • signal() :喚醒一個等待執行緒。該執行緒從等待方法返回前必須獲得與Condition相關的鎖。
  • signalAll() :喚醒所有等待執行緒。能夠從等待方法返回的執行緒必須獲得與Condition相關的鎖。

使用舉例:

/**
 * Condition使用範例
 *
 * @author 有夢想的肥宅
 */
public class ConditionTest {

    //1、建立一個Lock物件,Condition的使用需要依賴Lock物件
    public Lock lock = new ReentrantLock();
    //2、使用Lock物件的newCondition()方法來生成Condition物件
    public Condition condition = lock.newCondition();

    //3、main方法測試Condition的作用
    public static void main(String[] args) {
        ConditionTest conditionTest = new ConditionTest();
        //3.1、構造一個容量為2的固定執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        //3.2、執行conditionWait()方法使執行緒進入“等待”狀態
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                conditionTest.conditionWait();
            }
        });
        //3.3、conditionSignal()方法“喚醒”執行緒
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                conditionTest.conditionSignal();
            }
        });
    }

    /**
     * 執行緒等待
     */
    public void conditionWait() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "拿到鎖了");
            System.out.println(Thread.currentThread().getName() + "等待訊號");
            condition.await();//執行緒進入等待狀態,不進入finally語句塊進行鎖的釋放,要等待被喚醒
            System.out.println(Thread.currentThread().getName() + "拿到訊號");
        } catch (Exception e) {

        } finally {
            lock.unlock();
        }
    }

    /**
     * 執行緒喚醒
     */
    public void conditionSignal() {
        lock.lock();
        try {
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + "拿到鎖了");
            condition.signal();//喚醒執行緒
            System.out.println(Thread.currentThread().getName() + "發出訊號");
        } catch (Exception e) {

        } finally {
            lock.unlock();
        }
    }

}

三、ReentrantLock可重入鎖

  ReentrantLock:是一個可重入鎖,且它可以設定自身非公平鎖或者是公平鎖

  常用方法:

  • ReentrantLock() : 建立一個ReentrantLock例項【預設非公平鎖】
  • lock() : 獲得鎖
  • unlock() : 釋放鎖
/**
 * ReentrantLock測試類
 *
 * @author 有夢想的肥宅
 */
public class ReentrantLockTest {
    //全域性物件lock【構造引數設定為true表示為公平鎖,false或為空則預設是非公平鎖】
    private static Lock lock = new ReentrantLock(true);

    //執行緒方法
    public static void test() {
        for (int i = 0; i < 2; i++) {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "獲取了鎖");
                TimeUnit.MILLISECONDS.sleep(1000);//等待1秒,為了更直觀地觀察公平鎖的機制
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    //執行方法
    public static void main(String[] args) {
        System.out.println("=====公平鎖例項=====");
        //啟動一個名叫“執行緒A”的執行緒
        new Thread("執行緒A") {
            @Override
            public void run() {
                test();
            }
        }.start();
        //啟動一個名叫“執行緒B”的執行緒
        new Thread("執行緒B") {
            @Override
            public void run() {
                test();
            }
        }.start();
    }
}

ReentrantLock與synchronized的比較

相似點

  它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當如果一個執行緒獲得了物件鎖,進入了同步塊,其他訪問該同步塊的執行緒都必須阻塞在同步塊外面等待,等到釋放掉鎖或者喚醒後才能繼續獲得鎖。

區別

  1️⃣對於Synchronized來說,它是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現。而ReentrantLock它是JDK 1.5之後提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成

  2️⃣便利性:Synchronized的使用比較方便簡潔,並且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock需要手工宣告來加鎖和釋放鎖,為了避免忘記手工釋放鎖造成死鎖,所以最好在finally中宣告釋放鎖。

  3️⃣鎖的細粒度和靈活度:ReenTrantLock優於Synchronized【可以指定在哪加鎖和解鎖】

四、ReentrantReadWriteLock可重入讀寫鎖

  定義:ReentrantReadWriteLock是一種可重入讀寫鎖,內部有兩把鎖來實現讀和寫的鎖功能,在ReentrantLock的基礎上優化了效能,但是使用起來需要更加謹慎。

  性質:

可重入

  如果你瞭解過synchronized關鍵字,一定知道他的可重入性,可重入就是同一個執行緒可以重複加鎖,每次加鎖的時候count值加1,每次釋放鎖的時候count減1,直到count為0,其他的執行緒才可以再次獲取。

讀寫分離

  我們知道,對於一個數據,不管是幾個執行緒同時讀都不會出現任何問題,但是寫就不一樣了,幾個執行緒對同一個資料進行更改就可能會出現資料不一致的問題,因此想出了一個方法就是對資料加鎖,這時候出現了一個問題:執行緒寫資料的時候加鎖是為了確保資料的準確性,但是執行緒讀資料的時候再加鎖就會大大降低效率,這時候怎麼辦呢?那就對寫資料和讀資料分開,加上兩把不同的鎖,不僅保證了正確性,還能提高效率。

鎖可以降級

  執行緒獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。

鎖不可升級

  執行緒獲取讀鎖是不能直接升級為寫入鎖的。需要釋放所有讀取鎖,才可獲取寫鎖,我們理解了上面的概念之後,接下來我們看看如何去使用。

  使用示例:

/**
 * ReentrantReadWriteLock測試類【可重入讀寫鎖】
 *
 * @author 有夢想的肥宅
 */
public class ReentrantReadWriteLockTest {
    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);//全域性可重入讀寫鎖物件
    private final Lock readLock = reentrantReadWriteLock.readLock();//讀鎖
    private final Lock writeLock = reentrantReadWriteLock.writeLock();//寫鎖
    private final List<String> data = new ArrayList<>();//模擬被操作的資料

    /**
     * 寫資料的方法
     * @Description 使用writeLock獲取一把寫鎖,然後內部List寫入資料,最後在finally中釋放寫鎖。
     */
    public void write() {
        try {
            //1、加上寫鎖
            writeLock.lock();
            //2、操作公共資料
            data.add("寫資料");
            System.out.println("當前執行緒" + Thread.currentThread().getName() + "正在寫資料");
            //3、執行緒等待3秒
            Thread.sleep(3000);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            //4、釋放寫鎖
            writeLock.unlock();
        }
    }


    /**
     * 讀資料的方法
     * @Description 使用readLock獲取一把讀鎖,然後內部List讀取資料,最後再finally中釋放讀鎖。
     */
    public void read() {
        try {
            //1、加上寫鎖
            writeLock.lock();
            //2、讀取公共資料
            for (String str : data) {
                System.out.println("當前執行緒" + Thread.currentThread().getName() + "正在讀資料");
            }
            //3、執行緒等待3秒
            Thread.sleep(3000);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            //4、釋放讀鎖
            readLock.unlock();
        }
    }
}

參考資料: