ReentrantLock(重入鎖)功能詳解和應用演示

1. ReentrantLock簡介

jdk中獨佔鎖的實現除了使用關鍵字synchronized外,還可使用ReentrantLock。雖然在性能上ReentrantLock和synchronized沒有什麼區別,但ReentrantLock相比synchronized而言功能更加豐富,使用起來更爲靈活,也更適合複雜的併發場景。併發

2. ReentrantLock和synchronized的相同點

2.1 ReentrantLock是獨佔鎖且可重入的

  • 例子
public class ReentrantLockTest {

    public static void main(String[] args) throws InterruptedException {

        ReentrantLock lock = new ReentrantLock();

        for (int i = 1; i <= 3; i++) {
            lock.lock();
        }

        for(int i=1;i<=3;i++){
            try {

            } finally {
                lock.unlock();
            }
        }
    }
}

上面的代碼經過lock()方法先獲取鎖三次,而後經過unlock()方法釋放鎖3次,程序能夠正常退出。從上面的例子能夠看出,ReentrantLock是能夠重入的鎖,當一個線程獲取鎖時,還能夠接着重複獲取屢次。在加上ReentrantLock的的獨佔性,咱們能夠得出如下ReentrantLock和synchronized的相同點。ide

  • 1.ReentrantLock和synchronized都是獨佔鎖,只容許線程互斥的訪問臨界區。可是實現上二者不一樣:synchronized加鎖解鎖的過程是隱式的,用戶不用手動操做,優勢是操做簡單,但顯得不夠靈活。通常併發場景使用synchronized的就夠了;ReentrantLock須要手動加鎖和解鎖,且解鎖的操做盡可能要放在finally代碼塊中,保證線程正確釋放鎖。ReentrantLock操做較爲複雜,可是由於能夠手動控制加鎖和解鎖過程,在複雜的併發場景中能派上用場。性能

  • 2.ReentrantLock和synchronized都是可重入的。synchronized由於可重入所以能夠放在被遞歸執行的方法上,且不用擔憂線程最後可否正確釋放鎖;而ReentrantLock在重入時要卻確保重複獲取鎖的次數必須和重複釋放鎖的次數同樣,不然可能致使其餘線程沒法得到該鎖。測試

3. ReentrantLock相比synchronized的額外功能

3.1 ReentrantLock能夠實現公平鎖。

公平鎖是指當鎖可用時,在鎖上等待時間最長的線程將得到鎖的使用權。而非公平鎖則隨機分配這種使用權。和synchronized同樣,默認的ReentrantLock實現是非公平鎖,由於相比公平鎖,非公平鎖性能更好。固然公平鎖能防止飢餓,某些狀況下也頗有用。在建立ReentrantLock的時候經過傳進參數true建立公平鎖,若是傳入的是false或沒傳參數則建立的是非公平鎖this

ReentrantLock lock = new ReentrantLock(true);

繼續跟進看下源碼線程

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

能夠看到公平鎖和非公平鎖的實現關鍵在於成員變量sync的實現不一樣,這是鎖實現互斥同步的核心。之後有機會咱們再細講。code

  • 一個公平鎖的例子
public class ReentrantLockTest {

    static Lock lock = new ReentrantLock(true);

    public static void main(String[] args) throws InterruptedException {

        for(int i=0;i<5;i++){
            new Thread(new ThreadDemo(i)).start();
        }

    }

    static class ThreadDemo implements Runnable {
        Integer id;

        public ThreadDemo(Integer id) {
            this.id = id;
        }

        @Override

      public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<2;i++){
                lock.lock();
                System.out.println("得到鎖的線程:"+id);
                lock.unlock();
            }
        }
    }
}
  • 公平鎖結果

咱們開啓5個線程,讓每一個線程都獲取釋放鎖兩次。爲了能更好的觀察到結果,在每次獲取鎖前讓線程休眠10毫秒。能夠看到線程幾乎是輪流的獲取到了鎖。若是咱們改爲非公平鎖,再看下結果對象

  • 非公平鎖結果

線程會重複獲取鎖。若是申請獲取鎖的線程足夠多,那麼可能會形成某些線程長時間得不到鎖。這就是非公平鎖的「飢餓」問題。blog

  • 公平鎖和非公平鎖該如何選擇
    大部分狀況下咱們使用非公平鎖,由於其性能比公平鎖好不少。可是公平鎖可以避免線程飢餓,某些狀況下也頗有用。

3.2 .ReentrantLock可響應中斷

當使用synchronized實現鎖時,阻塞在鎖上的線程除非得到鎖不然將一直等待下去,也就是說這種無限等待獲取鎖的行爲沒法被中斷。而ReentrantLock給咱們提供了一個能夠響應中斷的獲取鎖的方法lockInterruptibly()。該方法能夠用來解決死鎖問題。

  • 響應中斷的例子
public class ReentrantLockTest {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new ThreadDemo(lock1, lock2));//該線程先獲取鎖1,再獲取鎖2
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//該線程先獲取鎖2,再獲取鎖1
        thread.start();
        thread1.start();
        thread.interrupt();//是第一個線程中斷
    }

    static class ThreadDemo implements Runnable {
        Lock firstLock;
        Lock secondLock;
        public ThreadDemo(Lock firstLock, Lock secondLock) {
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
            try {
                firstLock.lockInterruptibly();
                TimeUnit.MILLISECONDS.sleep(10);//更好的觸發死鎖
                secondLock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"正常結束!");
            }
        }
    }
}
  • 結果

構造死鎖場景:建立兩個子線程,子線程在運行時會分別嘗試獲取兩把鎖。其中一個線程先獲取鎖1在獲取鎖2,另外一個線程正好相反。若是沒有外界中斷,該程序將處於死鎖狀態永遠沒法中止。咱們經過使其中一個線程中斷,來結束線程間毫無心義的等待。被中斷的線程將拋出異常,而另外一個線程將能獲取鎖後正常結束。

3.3 獲取鎖時限時等待

ReentrantLock還給咱們提供了獲取鎖限時等待的方法tryLock(),能夠選擇傳入時間參數,表示等待指定的時間,無參則表示當即返回鎖申請的結果:true表示獲取鎖成功,false表示獲取鎖失敗。咱們可使用該方法配合失敗重試機制來更好的解決死鎖問題。

  • 更好的解決死鎖的例子
public class ReentrantLockTest {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new ThreadDemo(lock1, lock2));//該線程先獲取鎖1,再獲取鎖2
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//該線程先獲取鎖2,再獲取鎖1
        thread.start();
        thread1.start();
    }

    static class ThreadDemo implements Runnable {
        Lock firstLock;
        Lock secondLock;
        public ThreadDemo(Lock firstLock, Lock secondLock) {
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
            try {
                while(!lock1.tryLock()){
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                while(!lock2.tryLock()){
                    lock1.unlock();
                    TimeUnit.MILLISECONDS.sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"正常結束!");
            }
        }
    }
}
  • 結果

線程經過調用tryLock()方法獲取鎖,第一次獲取鎖失敗時會休眠10毫秒,而後從新獲取,直到獲取成功。第二次獲取失敗時,首先會釋放第一把鎖,再休眠10毫秒,而後重試直到成功爲止。線程獲取第二把鎖失敗時將會釋放第一把鎖,這是解決死鎖問題的關鍵,避免了兩個線程分別持有一把鎖而後相互請求另外一把鎖。

4. 結合Condition實現等待通知機制

使用synchronized結合Object上的wait和notify方法能夠實現線程間的等待通知機制。ReentrantLock結合Condition接口一樣能夠實現這個功能。並且相比前者使用起來更清晰也更簡單。

4.1 Condition使用簡介

Condition由ReentrantLock對象建立,而且能夠同時建立多個

static Condition notEmpty = lock.newCondition();

static Condition notFull = lock.newCondition();

Condition接口在使用前必須先調用ReentrantLock的lock()方法得到鎖。以後調用Condition接口的await()將釋放鎖,而且在該Condition上等待,直到有其餘線程調用Condition的signal()方法喚醒線程。使用方式和wait,notify相似。

  • 一個使用condition的簡單例子
public class ConditionTest {

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

        lock.lock();
        new Thread(new SignalThread()).start();
        System.out.println("主線程等待通知");
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
        System.out.println("主線程恢復運行");
    }
    static class SignalThread implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                condition.signal();
                System.out.println("子線程通知");
            } finally {
                lock.unlock();
            }
        }
    }
}
  • 運行結果

4.2 使用Condition實現簡單的阻塞隊列

阻塞隊列是一種特殊的先進先出隊列,它有如下幾個特色
1.入隊和出隊線程安全
2.當隊列滿時,入隊線程會被阻塞;當隊列爲空時,出隊線程會被阻塞。

  • 阻塞隊列的簡單實現
public class MyBlockingQueue<E> {

    int size;//阻塞隊列最大容量

    ReentrantLock lock = new ReentrantLock();

    LinkedList<E> list=new LinkedList<>();//隊列底層實現

    Condition notFull = lock.newCondition();//隊列滿時的等待條件
    Condition notEmpty = lock.newCondition();//隊列空時的等待條件

    public MyBlockingQueue(int size) {
        this.size = size;
    }

    public void enqueue(E e) throws InterruptedException {
        lock.lock();
        try {
            while (list.size() ==size)//隊列已滿,在notFull條件上等待
                notFull.await();
            list.add(e);//入隊:加入鏈表末尾
            System.out.println("入隊:" +e);
            notEmpty.signal(); //通知在notEmpty條件上等待的線程
        } finally {
            lock.unlock();
        }
    }

    public E dequeue() throws InterruptedException {
        E e;
        lock.lock();
        try {
            while (list.size() == 0)//隊列爲空,在notEmpty條件上等待
                notEmpty.await();
            e = list.removeFirst();//出隊:移除鏈表首元素
            System.out.println("出隊:"+e);
            notFull.signal();//通知在notFull條件上等待的線程
            return e;
        } finally {
            lock.unlock();
        }
    }
}
  • 測試代碼
public static void main(String[] args) throws InterruptedException {

    MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);
    for (int i = 0; i < 10; i++) {
        int data = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    queue.enqueue(data);
                } catch (InterruptedException e) {

                }
            }
        }).start();

    }
    for(int i=0;i<10;i++){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer data = queue.dequeue();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}
  • 運行結果

5. 總結

ReentrantLock是可重入的獨佔鎖。比起synchronized功能更加豐富,支持公平鎖實現,支持中斷響應以及限時等待等等。能夠配合一個或多個Condition條件方便的實現等待通知機制。

相關文章
相關標籤/搜索