多線程系列(八) -ReentrantLock基本用法介紹

一、簡介

在之前的線程系列文章中,我們介紹到了使用synchronized關鍵字可以實現線程同步安全的效果,以及採用wait()notify()notifyAll()方法,可以實現多個線程之間的通信協調,基本可以滿足併發編程的需求。

但是採用synchronized進行加鎖,這種鎖一般都比較重,裏面的實現機制也非常複雜,同時獲取鎖時必須一直等待,沒有額外的嘗試機制,如果編程不當,可能就容易發生死鎖現象。

從 JDK 1.5 開始,引入了一個高級的處理併發的java.util.concurrent包,它提供了大量更高級的併發功能,能大大的簡化多線程程序的編寫。

比如我們今天要介紹的java.util.concurrent.locks包提供的ReentrantLock類,一個可重入的互斥鎖,它具有與使用synchronized加鎖一樣的特性,並且功能更加強大。

下面我們一起來學習一下ReentrantLock類的基本使用。

二、ReentrantLock 基本用法

在介紹ReentrantLock之前,我們先來看一下傳統的使用synchronized對方法進行加鎖的示例。

public class Counter {

    private int count;

    public void add() {
        synchronized(this) {
            count ++;
            System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
        }
    }

    public int getCount() {
        return count;
    }
}
public static void main(String[] args) throws InterruptedException {
    Counter counter = new Counter();

    // 創建5個線程,同時對count進行加一操作
    for (int i = 0; i < 5; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.add();
            }
        }).start();
    }

    // 假設休眠1秒,5個線程執行完畢
    Thread.sleep(1000);
    System.out.println("count:" + counter.getCount());
}

輸出結果如下:

ThreadName:Thread-0, count:1
ThreadName:Thread-1, count:2
ThreadName:Thread-2, count:3
ThreadName:Thread-3, count:4
ThreadName:Thread-4, count:5
count:5

如果用ReentrantLock替代,只需要將Counter中的代碼改造爲如下:

public class Counter {

    private final Lock lock = new ReentrantLock();

    private int count;

    public void add() {
        // 加鎖
        lock.lock();
        try {
            count ++;
            System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }
    
    public int getCount() {
        return count;
    }
}

運行程序,結果與上面一致,可以證明:ReentrantLock具備與synchronized一樣的加鎖功能。

同時,ReentrantLock還具備在指定的時間內嘗試獲取鎖的機制,比如下面這行代碼:

if (lock.tryLock(3, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

嘗試在 3 秒內獲取鎖,如果獲取不到就返回false,程序不需要無限等待下去,這個功能在實際開發中使用非常的廣泛。

從上面的示例代碼,我們可以總結出synchronizedReentrantLock有以下幾點不一樣。

  • ReentrantLock需要手動調用加鎖方法;而synchronized不需要,它採用了隱藏的加鎖方式,藉助 jvm 來實現
  • synchronized不需要考慮異常;而ReentrantLock獲取鎖之後,要在finally中正確的釋放鎖,否則會影響其它線程
  • ReentrantLock擁有嘗試獲取鎖的超時機制,利用它可以避免無限等待;而synchronized不具備
  • synchronized是 Java 語言層面提供的語法;而ReentrantLock是 Java 代碼實現的可重入鎖

因此,在併發編程中,使用ReentrantLock比直接使用synchronized更靈活、更安全,採用tryLock(long time, TimeUnit unit)方法,即使未獲取到鎖也不會導致死鎖。

三、ReentrantLock 和 synchronized 持有的對象監視器是同一個嗎?

可能有的同學會發出這樣的一個問題,使用ReentrantLock進行加鎖和使用synchronized加鎖,兩者持有的對象監視器是同一個嗎?

下面我們一起來看一個例子。

public class Counter {

    private final Lock lock = new ReentrantLock();

    private int count;


    public synchronized void methodA() {
        System.out.println("ThreadName:" + Thread.currentThread().getName() + ",begin methodA, count:" + getCount());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count ++;
        System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());

    }

    public void methodB() {
        // 加鎖
        lock.lock();
        try {
            System.out.println("ThreadName:" + Thread.currentThread().getName() + ",begin methodB, count:" + getCount());
            Thread.sleep(3000);
            count ++;
            System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
        } catch (Exception e){
          e.printStackTrace();
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}
public class MyThreadA extends Thread {

    private Counter counter;

    public MyThreadA(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        counter.methodA();
    }
}
public class MyThreadB extends Thread {

    private Counter counter;

    public MyThreadB(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        counter.methodB();
    }
}
public class MyThreadTest {

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

        MyThreadA threadA = new MyThreadA(counter);
        threadA.start();

        MyThreadB threadB = new MyThreadB(counter);
        threadB.start();
    }
}

看一下運行結果:

ThreadName:Thread-0,begin methodA, count:0
ThreadName:Thread-1,begin methodB, count:0
ThreadName:Thread-0, count:2
ThreadName:Thread-1, count:2

從日誌上可以看出,採用兩個線程分別採用synchronizedReentrantLock兩種加鎖方式對count進行操作,兩個線程交替執行,可以得出一個結論:synchronizedReentrantLock持有的對象監視器不同。

四、Condition 基本用法

在之前的文章中,我們介紹了在synchronized同步方法/代碼塊中,使用wait()notify()notifyAll()可以實現線程之間的等待/通知模型。

ReentrantLock同樣也可以,只需要藉助Condition類即可實現,Condition提供的await()signal()signalAll()原理和synchronized鎖對象的wait()notify()notifyAll()是一致的,並且其行爲也是一樣的。

我們還是先來看一個簡單的示例。

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();
            // 喚醒所有等待線程
//            condition.signalAll();
            System.out.println("signal 喚醒通知完畢");
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

public class MyThreadA extends Thread {

    private Counter counter;

    public MyThreadA(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        counter.await();
    }
}
public class MyThreadB extends Thread {

    private Counter counter;

    public MyThreadB(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        counter.signal();
    }
}
public class MyThreadTest {

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

        // 先啓動執行等待的線程
        MyThreadA threadA = new MyThreadA(counter);
        threadA.start();

        Thread.sleep(3000);

        // 過3秒,再啓動執行通知的線程
        MyThreadB threadB = new MyThreadB(counter);
        threadB.start();
    }
}

看一下運行結果:

signal 通知完畢
await等待結束,count:1

從結果上看很明顯的看出,等待線程MyThreadA先啓動,過了 3 秒之後再啓動了MyThreadB,但是signal()方法先執行完畢,再通知await()方法執行,符合代碼預期。

這個例子也證明了一點:condition.await()方法是釋放了鎖,不然signal()方法體不會被執行。

相比wait/notify/notifyAll的等待/通知模型,Condition更加靈活,理由有以下幾點:

  • notify()方法喚醒線程時,被通知的線程由 Java 虛擬機隨機選擇;而採用ReentrantLock結合Condition可以實現有選擇性地通知,這一特性在實際編程中非常實用
  • 一個Lock裏面可以創建多個Condition實例,實現多路通知,使用多個Condition的應用場景很常見,比如ArrayBlockingQueue

五、小結

本文主要圍繞ReentrantLock的基本使用做了一次簡單的知識總結,如果有不正之處,請多多諒解,並歡迎批評指出。

六、參考

1、博客園 -五月的倉頡 - ReentrantLock的使用和Condition

2、 廖雪峯 - 使用ReentrantLock

七、寫到最後

最近無意間獲得一份阿里大佬寫的技術筆記,內容涵蓋 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多線程、JPA、MyBatis、MySQL 等技術知識。需要的小夥伴可以點擊如下鏈接獲取,資源地址:技術資料筆記

不會有人刷到這裏還想白嫖吧?點贊對我真的非常重要!在線求贊。加個關注我會非常感激!

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