多線程系列(九) -ReentrantLock常用方法詳解

来源:https://www.cnblogs.com/dxflqm/p/18033936
-Advertisement-
Play Games

在上一篇文章中,我們介紹了ReentrantLock類的一些基本用法,今天我們重點來介紹一下ReentrantLock其它的常用方法,以便對ReentrantLock類的使用有更深入的理解。 ...


一、簡介

在上一篇文章中,我們介紹了ReentrantLock類的一些基本用法,今天我們重點來介紹一下ReentrantLock其它的常用方法,以便對ReentrantLock類的使用有更深入的理解。

二、常用方法介紹

2.1、構造方法

ReentrantLock類有兩個構造方法,核心源碼內容如下:

/**
 * 預設創建非公平鎖
 */
public ReentrantLock() {
    sync = new NonfairSync();
}
/**
 * fair為true表示是公平鎖,fair為false表示是非公平鎖
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

相比於synchronized同步鎖,ReentrantLock有一個很大的特點,就是開發人員可以手動指定採用公平鎖機制還是非公平鎖機制。

公平鎖:顧名思義,就是每個線程獲取鎖的順序是按照線程排隊的順序來分配的,最前面的線程總是最先獲取到鎖。

  • 優點:所有的線程都有機會得到資源
  • 缺點:公平鎖機制實現比較複雜,程式流程比較多,執行速度比較慢

非公平鎖:每個線程獲取鎖的順序是隨機的,並不會遵循先來先得的規則,任何線程在某時刻都有可能直接獲取並擁有鎖,之前介紹的synchronized其實就是一種非公平鎖

  • 優點:公平鎖機制實現相對比較簡單,程式流程比較少,執行速度比較快
  • 缺點:有可能某些線程一直拿不到鎖,導致餓死

ReentrantLock預設的構造方法是非公平鎖,如果想要構造公平鎖機制,只需要傳入true就可以了。

示例代碼如下:

public static void main(String[] args) {
    // 創建公平鎖實現機制
    Lock lock = new ReentrantLock(true);

    // 創建5個線程
    for (int i = 0; i < 5; i++) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 啟動了!");

                // 嘗試獲取鎖
                lock.lock();
                try {
                    System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 獲得鎖!");
                } finally {
                    lock.unlock();
                }
            }
        }).start();
    }
}

運行一下程式,結果如下:

ThreadName:Thread-0, 啟動了!
ThreadName:Thread-1, 啟動了!
ThreadName:Thread-0, 獲得鎖!
ThreadName:Thread-1, 獲得鎖!
ThreadName:Thread-2, 啟動了!
ThreadName:Thread-2, 獲得鎖!
ThreadName:Thread-3, 啟動了!
ThreadName:Thread-3, 獲得鎖!
ThreadName:Thread-4, 啟動了!
ThreadName:Thread-4, 獲得鎖!

從日誌上可以看到,啟動順序為0,1,2,3,4,獲取鎖的順序為0,1,2,3,4,啟動與獲取鎖的排隊機制一致。

假如我們構造方法裡面的把true改成false,也就是非公平鎖機制,在看看運行效果,結果如下:

ThreadName:Thread-1, 啟動了!
ThreadName:Thread-2, 啟動了!
ThreadName:Thread-1, 獲得鎖!
ThreadName:Thread-0, 啟動了!
ThreadName:Thread-2, 獲得鎖!
ThreadName:Thread-3, 啟動了!
ThreadName:Thread-3, 獲得鎖!
ThreadName:Thread-0, 獲得鎖!
ThreadName:Thread-4, 啟動了!
ThreadName:Thread-4, 獲得鎖!

從日誌上可以看到,啟動順序為1,2,0,3,4,獲取鎖的順序為1,2,3,0,4,線程啟用與獲取鎖的順序不一致。

從實際的運行結果看,非公平鎖要比公平鎖執行速度要快一些,當線程數越多的時候,效果越明顯。

2.2、核心方法

ReentrantLock類的核心方法就比較多了,如下表!

方法 描述
public void lock() 阻塞等待獲取鎖;不允許Thread.interrupt中斷,即使檢測到Thread.isInterrupted一樣會繼續嘗試
public void lockInterruptibly() 當前線程未被中斷,則獲取鎖;允許在等待時由其它線程調用等待線程的Thread.interrupt方法來中斷等待線程的等待而直接返回
public boolean tryLock() 嘗試申請一個鎖,在成功獲得鎖後返回true,否則,立即返回false
public boolean tryLock(long timeout, TimeUnit unit) 在一段時間內嘗試申請一個鎖,在成功獲得鎖後返回true,否則,立即返回false
public void unlock() 釋放鎖
public Condition newCondition() 條件實例,用於線程等待/通知模式
public int getHoldCount() 獲取當前線程持有此鎖的次數
public boolean isHeldByCurrentThread() 檢測是否被當前線程持有
public boolean isLocked() 查詢此鎖是否由任意線程持有
public final boolean isFair() 如果是公平鎖返回true,否則返回false
public final boolean hasQueuedThreads() 查詢是否有線程正在等待
public final boolean hasQueuedThread(Thread thread) 查詢給定線程是否正在等待獲取此鎖
public final int getQueueLength() 獲取正等待獲取此鎖的線程數
public boolean hasWaiters(Condition condition) 是否存在正在等待並符合相關給定條件的線程
public int getWaitQueueLength(Condition condition) 正在等待並符合相關給定條件的線程數量

雖然方法很多,但是實際上常用方法就那麼幾個,下麵我們主要抽一些常用的方法進行介紹。

2.2.1、tryLock 方法

lock()lockInterruptibly()tryLock()tryLock(long timeout, TimeUnit unit)這幾個方法,目的其實是一樣的,都是為了獲取鎖,只是針對不同的場景做了單獨的處理。

lock():阻塞等待獲取鎖,如果沒有獲取到會一直阻塞,即使檢測到Thread.isInterrupted一樣會繼續嘗試;

  • lockInterruptibly():同樣也是阻塞等待獲取鎖,稍有不同的是,允許在等待時由其它線程調用等待線程的Thread.interrupt方法來中斷等待線程的等待而直接返回
  • tryLock():表示嘗試申請一個鎖,在成功獲得鎖後返回true,否則,立即返回false,不會阻塞等待獲取鎖
  • tryLock(long timeout, TimeUnit unit):表示在一段時間內嘗試申請一個鎖,在成功獲得鎖後返回true,否則,立即返回false

其中tryLock(long timeout, TimeUnit unit)方法的應用最廣泛,因為它能防止程式發生死鎖,即使在一段時間內沒有獲取鎖,也會自動退出,不會一直阻塞。

我們可以看一個簡單的例子,如下!

public static void main(String[] args) {
    // 創建公平鎖實現機制
    Lock lock = new ReentrantLock();

    // 創建5個線程
    for (int i = 0; i < 5; i++) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                boolean flag = false;
                try {
                    // 嘗試3秒內獲取鎖
                    flag = lock.tryLock(3, TimeUnit.SECONDS);
                    if(flag){
                        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 獲取到鎖");
                        // 模擬進行5秒的業務操作
                        Thread.sleep(5000);
                    } else {
                        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 經過3秒鐘的嘗試未獲取到鎖,放棄嘗試");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (flag){
                        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 釋放對象");
                        lock.unlock();
                    }
                }
            }
        }).start();
    }
}

運行一下程式,結果如下:

ThreadName:Thread-0, 獲取到鎖
ThreadName:Thread-3, 經過3秒鐘的嘗試未獲取到鎖,放棄嘗試
ThreadName:Thread-1, 經過3秒鐘的嘗試未獲取到鎖,放棄嘗試
ThreadName:Thread-2, 經過3秒鐘的嘗試未獲取到鎖,放棄嘗試
ThreadName:Thread-4, 經過3秒鐘的嘗試未獲取到鎖,放棄嘗試
ThreadName:Thread-0, 釋放對象

可以很清晰的看到,非Thread-0線程嘗試了 3 秒沒有獲取到鎖,自動放棄;如果換成lock()方法進行獲取鎖,線程Thread-0如果不釋放鎖,其它線程會一直阻塞。

2.2.2、unlock 方法

unlock()方法也是常用方法,表示釋放鎖。當獲取到鎖之後,一定要手動釋放鎖,否則可能會造成其它程式執行出現問題,通常用在finally方法塊裡面。

// 阻塞等待獲取鎖
lock.lock();
try {
    // 業務操作...
} finally {
	// 一定要釋放鎖
    lock.unlock();
}
2.2.3、newCondition 方法

newCondition()方法,在上文中介紹過,ReentrantLockCondition結合,可以實現線程之間的等待/通知模型。

簡單的示例,如下!

public class Counter {

    private final Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    private int count;

    public void await(){
        // 加鎖
        lock.lock();
        try {
            // 讓當前線程進入等待狀態,並釋放鎖
            condition.await();
            System.out.println("await等待結束,count:" + getCount());
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }


    public void signal(){
        // 加鎖
        lock.lock();
        try {
            count++;
            // 喚醒某個等待線程
            condition.signal();
            System.out.println("signal 喚醒通知完畢");
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}
public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 先啟動執行等待的線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.await();
            }
        }).start();

        Thread.sleep(3000);

        // 過3秒,再啟動執行通知的線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.signal();
            }
        }).start();
    }
}

運行一下程式,結果如下:

signal 喚醒通知完畢
await等待結束,count:1
2.2.4、getHoldCount 方法

getHoldCount()方法的作用是返回的是當前線程調用lock()的次數。

示例代碼如下:

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();

    new Thread(new Runnable() {

        @Override
        public void run() {
            // 第一次獲取鎖
            lock.lock();
            try {
                System.out.println("ThreadName:" + Thread.currentThread().getName() + ", getHoldCount:" +  lock.getHoldCount());

                // 第二次獲取鎖
                lock.lock();
                try {
                    System.out.println("ThreadName:" + Thread.currentThread().getName() + ", getHoldCount:" +  lock.getHoldCount());
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        }
    }).start();
}

運行一下程式,結果如下:

ThreadName:Thread-0, getHoldCount:1
ThreadName:Thread-0, getHoldCount:2

側面也證明瞭一點,ReentrantLocksynchronized一樣,鎖都具有可重入特性,也就是說同一個線程多次調用同一個ReentrantLocklock()方法,可以再次進入方法體,無需阻塞等待。

2.2.5、isLocked 方法

isHeldByCurrentThread()isLocked()方法都是用於檢測鎖是否被持有。

其中isHeldByCurrentThread()方法表示此鎖是否由當前線程持有;isLocked()方法表示此鎖是否由任意線程持有。

我們看一個簡單的示例,如下:

public class Counter {

    private ReentrantLock lock = new ReentrantLock();

    public void methodA(){
        lock.lock();
        try {
            System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 當前線程是否持有鎖:" +  lock.isHeldByCurrentThread());
            System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 任意線程是否持有鎖:" +  lock.isLocked());
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void methodB(){
        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 當前線程是否持有鎖:" +  lock.isHeldByCurrentThread());
        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", 任意線程是否持有鎖:" +  lock.isLocked());
    }
}
public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.methodA();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.methodB();
            }
        }).start();
    }
}

運行一下程式,結果如下:

ThreadName:Thread-0, 當前線程是否持有鎖:true
ThreadName:Thread-0, 任意線程是否持有鎖:true
ThreadName:Thread-1, 當前線程是否持有鎖:false
ThreadName:Thread-1, 任意線程是否持有鎖:true

從日誌結果很容易理解,Thread-0線程持有鎖,因此調用isHeldByCurrentThread()isLocked()方法,返回結果都是trueThread-1線程沒有持有鎖,因此isHeldByCurrentThread()方法返回falseisLocked()方法返回true

2.2.6、isFair 方法

isFair()方法用來獲取此鎖是否公平鎖。

簡單的示例,如下:

ReentrantLock lock = new ReentrantLock(true);
System.out.println("是否公平鎖:" +  lock.isFair());

輸出結果如下:

是否公平鎖:true

ReentrantLock預設的是非公平鎖,當通過構造方法顯式傳入true時,採用的是公平鎖機制

2.2.5、hasQueuedThreads 方法

hasQueuedThreads()hasQueuedThread()方法都用於查詢是否有線程等待獲取鎖,稍有不同的是:hasQueuedThreads()方法表示查詢是否有線程正在等待獲取鎖;hasQueuedThread()方法表示查詢給定線程是否正在等待獲取此鎖。

另外還有一個getQueueLength()方法,表示獲取正等待獲取此鎖的線程數。

我們看一個簡單的示例,如下:

public static void main(String[] args) throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();

    Thread threadA = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    threadA.start();

    Thread threadB = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    threadB.start();

    // 等待線程都啟動完畢
    Thread.sleep(1000);

    System.out.println("查詢是否有線程正在等待:" + lock.hasQueuedThreads());
    System.out.println("查詢處於等待的線程數:" + lock.getQueueLength());
    System.out.println("threadA 是否處於等待狀態:" +  lock.hasQueuedThread(threadA));
    System.out.println("threadB 是否處於等待狀態:" +  lock.hasQueuedThread(threadB));
}

輸出結果如下:

查詢是否有線程正在等待:true
查詢處於等待的線程數:1
threadA 是否處於等待狀態:false
threadB 是否處於等待狀態:true

從日誌上可以清晰的看到,線程threadA先獲取了鎖,線程threadB處於等待獲取鎖的狀態,處於等待的線程數為1

2.2.7、hasWaiters 方法

hasWaiters()getWaitQueueLength()方法,支持傳入condition條件對象進行查詢。

其中hasWaiters()方法表示查詢是否存在正在等待並符合相關給定條件的線程;getWaitQueueLength()方法表示查詢正在等待並符合相關給定條件的線程數量。

我們看一個簡單的示例,如下:

public static void main(String[] args) throws InterruptedException {
    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    Thread threadA = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            try {
                condition.await();
                System.out.println("await等待結束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    threadA.start();

    // 睡眠1秒
    Thread.sleep(1000);

    Thread threadB = new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println("是否存在正在等待並符合相關給定條件的線程:" + lock.hasWaiters(condition));
                System.out.println("正在等待並符合相關給定條件的線程數量:" + lock.getWaitQueueLength(condition));
                Thread.sleep(5000);
                condition.signal();
                System.out.println("signal 喚醒通知完畢");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    });
    threadB.start();
}

輸出結果如下:

是否存在正在等待並符合相關給定條件的線程:true
正在等待並符合相關給定條件的線程數量:1
signal 喚醒通知完畢
await等待結束

需要註意的是,調用condition對象的方法,必須要在獲取鎖的方法體內執行。

三、小結

本文主要圍繞ReentrantLock類的核心方法進行了一些知識總結,其中最常用方法的主要就兩個,tryLock(long timeout, TimeUnit unit)unlock(),通過它可以實現線程同步安全的效果。

本文內容比較多,如果有不正之處,請多多諒解,並歡迎批評指出。

四、參考

1、https://www.cnblogs.com/xrq730/p/4855538.html


作者:程式員志哥
出處:pzblog.cn
資源:微信搜【程式員志哥】關註我,回覆 【技術資料】有我準備的一線程式必備電腦書籍、大廠面試資料和免費電子書。 希望可以幫助大家提升技術和能力。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 寫在前面 我知道自己現在的狀態很不好,以為放個假能好好放鬆下心情,結果昨晚做夢還在工作,調試代碼,和領導彙報工作。 天吶,明明是在放假,可大腦還在考慮工作的事,我的天那,這是怎麼了? Vue頁面參數傳遞 1、任務拆解 頁面跳轉時帶上當前電子書id參數ebookId 新增/編輯文檔時,讀取電子書id參 ...
  • 枚舉Enum是在多種語言中都有的一種數據類型,用於表示一組特定相關的常量數據集合,如性別(男、女)、數據狀態(可用、禁用)、垂直對齊(頂端、居中、底部)、星期等。特點是數據值固定,不會變,存儲和顯示的內容不同。然而在JavaScript中並沒有枚舉Enum類型,TypeScript算是有(本文中暫沒... ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、是什麼 許可權是對特定資源的訪問許可,所謂許可權控制,也就是確保用戶只能訪問到被分配的資源 而前端許可權歸根結底是請求的發起權,請求的發起可能有下麵兩種形式觸發 頁面載入觸發 頁面上的按鈕點擊觸發 總的來說,所有的請求發起都觸發自前端路由或 ...
  • 即使再小再簡單的需求,作為研發開發完畢之後,我們可以直接上線麽?其實很多時候事故往往就是由於“不以為意”發生的。事故的發生往往也遵循“墨菲定律”,這就要求我們更要敬畏線上,再小的需求點都需要經過嚴格的測試驗證才能上線。 ...
  • 指針和引用 當我們需要在程式中傳遞變數的地址時,可以使用指針或引用。它們都可以用來間接訪問變數,但它們之間有一些重要的區別。 指針是一個變數,它存儲另一個變數的地址。通過指針,我們可以訪問存儲在該地址中的變數。指針可以被重新分配,可以指向不同的變數,也可以為NULL。指針使用*運算符來訪問存儲在地址 ...
  • 與TXT文本文件,PDF文件更加專業也更適合傳輸,常用於正式報告、簡歷、合同等場合。項目中如果有使用Java將TXT文本文件轉為PDF文件的需求,可以查看本文中介紹的免費實現方法。 免費Java PDF庫 本文介紹的方法需要用到Free Spire.PDF for Java,該免費庫支持多種操作、轉 ...
  • 雲採用框架(Cloud Adoption Framework,簡稱CAF)為企業上雲提供策略和技術的指導原則和最佳實踐,幫助企業上好雲、用好雲、管好雲,併成功實現業務目標。本雲採用框架是基於服務大量企業客戶的經驗總結,將企業雲採用分為四個階段,並詳細探討企業應在每個階段採取的業務和技術策略;同時,還 ...
  • Excelize 是 Go 語言編寫的用於操作電子錶格辦公文檔的開源基礎庫,2024年2月26日,社區正式發佈了 2.8.1 版本,該版本包含了多項新增功能、錯誤修複和相容性提升優化。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...