天天看點

Java并發程式設計:ReentrantLock重入鎖功能介紹

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

1.ReentrantLock和synchronized的相同點

1.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 {
                System.out.println(i);
            } finally {
                lock.unlock();
            }
        }
    }
}
           

上面的代碼通過

lock()

方法先擷取鎖三次,然後通過

unlock()

方法釋放鎖3次,程式可以正常退出。從上面的例子可以看出,ReentrantLock是可以重入的鎖,當一個線程擷取鎖時,還可以接着重複擷取多次。在加上ReentrantLock的的獨占性,我們可以得出以下ReentrantLock和synchronized的相同點。

  • 1.ReentrantLock和synchronized都是獨占鎖,隻允許線程互斥的通路臨界區。但是實作上兩者不同:synchronized加鎖解鎖的過程是隐式的,使用者不用手動操作,優點是操作簡單,但顯得不夠靈活。一般并發場景使用synchronized的就夠了;ReentrantLock需要手動加鎖和解鎖,且解鎖的操作盡量要放在finally代碼塊中,保證線程正确釋放鎖。ReentrantLock操作較為複雜,但是因為可以手動控制加鎖和解鎖過程,在複雜的并發場景中能派上用場。
  • 2.ReentrantLock和synchronized都是可重入的。synchronized因為可重入是以可以放在被遞歸執行的方法上,且不用擔心線程最後能否正确釋放鎖;而ReentrantLock在重入時要卻確定重複擷取鎖的次數必須和重複釋放鎖的次數一樣,否則可能導緻其他線程無法獲得該鎖。

2.ReentrantLock相比synchronized的額外功能

2.1 ReentrantLock可以實作公平鎖

公平鎖是指當鎖可用時,在鎖上等待時間最長的線程将獲得鎖的使用權。而非公平鎖則随機配置設定這種使用權。和synchronized一樣,預設的ReentrantLock實作是非公平鎖,因為相比公平鎖,非公平鎖性能更好。當然公平鎖能防止饑餓,某些情況下也很有用。在建立ReentrantLock的時候通過傳進參數

true

建立公平鎖,如果傳入的是

false

或沒傳參數則建立的是非公平鎖

ReentrantLock lock = new ReentrantLock(true);
           
ReentrantLock源碼:      
public class ReentrantLock implements Lock, Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    private final ReentrantLock.Sync sync;

    public ReentrantLock() {
        this.sync = new ReentrantLock.NonfairSync();
    }

    public ReentrantLock(boolean var1) {
        this.sync = (ReentrantLock.Sync)(var1 ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
    }
           

可以看到公平鎖和非公平鎖的實作關鍵在于成員變量

sync

的實作不同,這是鎖實作互斥同步的核心。

一個公平鎖的例子

public class ReentrantLockTest {
    static ReentrantLock 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(10000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            for (int i = 0; i < 2; i++){
                lock.lock();
                System.out.println("獲得鎖的線程:" + id);
                lock.unlock();
            }
        }
    }
}
           

公平鎖結果

Java并發程式設計:ReentrantLock重入鎖功能介紹

我們開啟5個線程,讓每個線程都擷取釋放鎖兩次。為了能更好的觀察到結果,在每次擷取鎖前讓線程休眠10秒。可以看到線程幾乎是輪流的擷取到了鎖。如果我們改成非公平鎖,再看下結果

Java并發程式設計:ReentrantLock重入鎖功能介紹

線程會重複擷取鎖。如果申請擷取鎖的線程足夠多,那麼可能會造成某些線程長時間得不到鎖。這就是非公平鎖的“饑餓”問題。

  • 公平鎖和非公平鎖該如何選擇

    大部分情況下我們使用非公平鎖,因為其性能比公平鎖好很多。但是公平鎖能夠避免線程饑餓,某些情況下也很有用。

2.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();
        thread1.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()+"正常結束!");
            }
        }
    }
}
           

結果:

Java并發程式設計:ReentrantLock重入鎖功能介紹

構造死鎖場景:建立兩個子線程,子線程在運作時會分别嘗試擷取兩把鎖。其中一個線程先擷取鎖1在擷取鎖2,另一個線程正好相反。如果沒有外界中斷,該程式将處于死鎖狀态永遠無法停止。我們通過使其中一個線程中斷,來結束線程間毫無意義的等待。被中斷的線程将抛出異常,而另一個線程将能擷取鎖後正常結束。

2.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()+"正常結束!");
            }
        }
    }
}
           

結果

Java并發程式設計:ReentrantLock重入鎖功能介紹

線程通過調用

tryLock()

方法擷取鎖,第一次擷取鎖失敗時會休眠10毫秒,然後重新擷取,直到擷取成功。第二次擷取失敗時,首先會釋放第一把鎖,再休眠10毫秒,然後重試直到成功為止。線程擷取第二把鎖失敗時将會釋放第一把鎖,這是解決死鎖問題的關鍵,避免了兩個線程分别持有一把鎖然後互相請求另一把鎖。

3 結合Condition實作等待通知機制

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

3.1 Condition由ReetrantLock對象建立,并且可以同時建立多個

static Condition notEmpty = lock.newCondition();

static Condition notFull = lock.newCondition();
           

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

  • 一個Condition對象的signal(signalAll)方法和該對象的await方法是一一對應的,也就是一個Condition對象的signal(signalAll)方法不能喚醒其他condition對象的await方法
  • Condition類可以喚醒指定條件的線程,而object的喚醒是随機的
  • Condition類的awiat方法和Object類的wait方法等效
  • Condition類的signal方法和Object類的notify方法等效
  • Condition類的signalAll方法和Object類的notifyAll方法等效

一個使用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();
            }
        }
    }
}
           

運作結果:

Java并發程式設計:ReentrantLock重入鎖功能介紹

3.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 class MyBlockingQueueTest {
    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();
        }

    }
}
           

運作結果

Java并發程式設計:ReentrantLock重入鎖功能介紹

4.ReentrantLock類的方法

  • getHoldCount() 查詢目前線程保持此鎖的次數,也就是執行此線程執行lock方法的次數
  • getQueueLength()傳回正等待擷取此鎖的線程估計數,比如啟動10個線程,1個線程獲得鎖,此時傳回的是9
  • getWaitQueueLength(Condition condition)傳回等待與此鎖相關的給定條件的線程估計數。比如10個線程,用同一個condition對象,并且此時這10個線程都執行了condition對象的await方法,那麼此時執行此方法傳回10
  • hasWaiters(Condition condition)查詢是否有線程等待與此鎖有關的給定條件(condition),對于指定contidion對象,有多少線程執行了condition.await方法
  • hasQueuedThread(Thread thread)查詢給定線程是否等待擷取此鎖
  • hasQueuedThreads()是否有線程等待此鎖
  • isFair()該鎖是否公平鎖
  • isHeldByCurrentThread() 目前線程是否保持鎖鎖定,線程的執行lock方法的前後分别是false和true
  • isLock()此鎖是否有任意線程占用
  • lockInterruptibly()如果目前線程未被中斷,擷取鎖
  • tryLock()嘗試獲得鎖,僅在調用時鎖未被線程占用,獲得鎖
  • tryLock(long timeout TimeUnit unit)如果鎖在給定等待時間内沒有被另一個線程保持,則擷取該鎖

5.總結

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

參考文章:

1.https://www.cnblogs.com/takumicx/p/9338983.html(很優秀)

2.https://www.cnblogs.com/-new/p/7256297.html