多線程系列(九) -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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章