1. 程式人生 > 其它 >鎖---ReentrantLock(獨佔鎖、可重入鎖)

鎖---ReentrantLock(獨佔鎖、可重入鎖)

技術標籤:併發程式設計

鎖—ReentrantLock(獨佔鎖、可重入鎖)

************ 如有侵權請提示刪除 ***************

文章目錄


### 概念: java除了使用關鍵字synchronized外,還可以使用ReentrantLock實現獨佔鎖的功能。而且ReentrantLock相比synchronized而言功能更加豐富,使用起來更為靈活,也更適合複雜的併發場景。這篇文章主要是從使用的角度來分析一下ReentrantLock。

簡介

ReentrantLock常常對比著synchronized來分析,我們先對比著來看然後再一點一點分析。

(1)synchronized是獨佔鎖,加鎖和解鎖的過程自動進行,易於操作,但不夠靈活。ReentrantLock也是獨佔鎖,加鎖和解鎖的過程需要手動進行,不易操作,但非常靈活。

(2)synchronized可重入,因為加鎖和解鎖自動進行,不必擔心最後是否釋放鎖;ReentrantLock也可重入,但加鎖和解鎖需要手動進行,且次數需一樣,否則其他執行緒無法獲得鎖。

(3)synchronized不可響應中斷,一個執行緒獲取不到鎖就一直等著;ReentrantLock可以相應中斷。

ReentrantLock好像比synchronized關鍵字沒好太多,我們再去看看synchronized所沒有的,一個最主要的就是ReentrantLock還可以實現公平鎖機制。什麼叫公平鎖呢?也就是在鎖上等待時間最長的執行緒將獲得鎖的使用權。通俗的理解就是誰排隊時間最長誰先執行獲取鎖

使用

簡單使用

我們先給出一個最基礎的使用案例,也就是實現鎖的功能。

public class TestReentrantLock {
    //非公平鎖  等價於new ReentrantLock(false)
    private static final Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        //測試-1   若果註釋掉1、2處,未釋放鎖就可能有執行緒獲取到鎖
        new Thread(() ->test(),"執行緒A"
).start(); new Thread(() ->test(),"執行緒B").start(); new Thread(() ->test(),"執行緒C").start(); } public static void test(){ try { lock.lock(); //1 System.out.println (Thread.currentThread().getName() + "獲取了鎖"); TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println (Thread.currentThread().getName() + "釋放了鎖"); lock.unlock();//2 } } }

輸出效果:

//test1效果  非公平鎖   cpu時間片輪到哪個執行緒,哪個執行緒就能獲取鎖
//執行緒A獲取了鎖
//執行緒A釋放了鎖
//執行緒C獲取了鎖
//執行緒C釋放了鎖
//執行緒B獲取了鎖
//執行緒B釋放了鎖

在這裡我們定義了一個ReentrantLock,然後再test方法中分別lock和unlock,執行一邊就可以實現我們的功能。這就是最簡單的功能實現,程式碼很簡單。我們再看看ReentrantLock和synchronized不一樣的地方,那就是公平鎖的實現。

公平鎖實現

對於公平鎖的實現,就要結合著我們的可重入性質了。公平鎖的含義我們上面已經說了,就是誰等的時間最長,誰就先獲取鎖。

public class TestReentrantLock {
   //實現公平鎖機制
    private static final ReentrantLock lock1 = new ReentrantLock(true);
    public static void main(String[] args) {
        //測試-2    公平鎖實現
        new Thread(() ->test2(),"執行緒A").start();
        new Thread(() ->test2(),"執行緒B").start();
        new Thread(() ->test2(),"執行緒C").start();
        new Thread(() ->test2(),"執行緒D").start();
        new Thread(() ->test2(),"執行緒E").start();
    }
    //公平鎖實現
    public static void test2(){
        for (int i = 0; i < 2; i++) {
            try {
                lock1.lock();
                System.out.println (Thread.currentThread().getName() + "獲取了鎖");
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock1.unlock();
            }
        }
    }
}

首先new一個ReentrantLock的時候引數為true,表明實現公平鎖機制。在這裡我們多定義幾個執行緒ABCDE,然後再test方法中迴圈執行了兩次加鎖和解鎖的過程。

輸出效果:

//test2效果   公平鎖
//執行緒A獲取了鎖
//執行緒B獲取了鎖
//執行緒C獲取了鎖
//執行緒D獲取了鎖
//執行緒E獲取了鎖
//執行緒A獲取了鎖
//執行緒B獲取了鎖
//執行緒C獲取了鎖
//執行緒D獲取了鎖
//執行緒E獲取了鎖

非公平鎖實現

非公平鎖那就隨機的獲取,誰運氣好,cpu時間片輪到哪個執行緒,哪個執行緒就能獲取鎖,和上面公平鎖的區別很簡單,就在於先new一個ReentrantLock的時候引數為false,當然我們也可以不寫,預設就是false。直接測試一下
輸出效果:

//test2效果   非公平鎖  整個過程隨機的,沒有先後
//執行緒A獲取了鎖
//執行緒B獲取了鎖
//執行緒B獲取了鎖
//執行緒C獲取了鎖
//執行緒C獲取了鎖
//執行緒D獲取了鎖
//執行緒D獲取了鎖
//執行緒E獲取了鎖
//執行緒A獲取了鎖
//執行緒E獲取了鎖

響應中斷

中斷響應是鎖申請等待過程中可以放棄等待轉去處理中斷

響應中斷就是一個執行緒獲取不到鎖,不會一直等下去,ReentrantLock會給予一箇中斷迴應。在這裡我們舉一個死鎖的案例。

首先我們定義一個測試類ReentrantLockTest3。

public class TestReentrantLock {
    public static void main(String[] args) {
       Thread thread1 = new Thread(new ThreadDemo(1), "thread1");
        Thread thread2 = new Thread(new ThreadDemo(2), "thread2");

        thread1.start();
        thread2.start();
        thread2.interrupt();//中斷第一個執行緒
    }
    static class ThreadDemo implements Runnable{
       //例項化兩把鎖
        public static ReentrantLock firstLock = new ReentrantLock();
        public static ReentrantLock secondLock = new ReentrantLock();
        int lock = 0;//標識先請求那把鎖

        //控制加鎖順序
        public ThreadDemo(int lock) {
            lock = lock;
        }

        @Override
        public void run() {
            try {
                if (lock == 1) {
                    //lockInterruptibly()是對中斷進行響應的申請動作
                    firstLock.lockInterruptibly();
                    //假設先獲得firstLock鎖,睡一秒,再請求secondLock鎖
                    TimeUnit.MILLISECONDS.sleep(2000);
                    secondLock.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName()
                            + "執行完成");
                } else {
                    //lockInterruptibly()是對中斷進行響應的申請動作
                    secondLock.lockInterruptibly();
                    //假設先獲得firstLock鎖,睡一秒,再請求secondLock鎖
                    TimeUnit.MILLISECONDS.sleep(2000);
                    firstLock.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName()
                            + "執行完成");
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //查詢當前執行緒是否保持此鎖,此方法通常用於除錯和測試
                if (firstLock.isHeldByCurrentThread()) {
                    firstLock.unlock();
                }
                if(secondLock.isHeldByCurrentThread()){
                    secondLock.unlock();
                }
                System.out.println(Thread.currentThread().getName()
                        + "執行緒退出");
            }
        }
    }
    
}

程式碼要做的就是例項化兩個執行緒,執行緒1先申請lock1鎖,睡眠一秒後再申請lock2鎖;執行緒2則相反,先申請lock2鎖,完了睡眠一秒再申請lock1鎖。當兩個執行緒都執行後,會造成死鎖,執行緒1佔有lock1鎖同時申請lock2鎖,執行緒2佔有lock2鎖同時申請lock1鎖, 此時兩個執行緒產生死鎖,不過在主執行緒中第70行我們對執行緒2做中斷操作,執行緒2會放棄對鎖lock1的申請,同時釋放鎖lock2,所以執行緒1會得到鎖lock2,併成功退出。

輸出結果如下:

thread1執行完成
thread1執行緒退出
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.mmz.tkp.controller.thread.locks.reentrantlock.TestReentrantLock$ThreadDemo.run(TestReentrantLock.java:128)
	at java.lang.Thread.run(Thread.java:748)
thread2執行緒退出

由輸出結果可以看出,執行緒2沒有輸出執行完成的語句,也就是說執行緒2並沒有獲得鎖lock1,線上程2響應中斷後,放棄了對鎖lock1的申請而直接執行下面的語句,也就是輸出退出語句。

執行緒1獲得鎖lock1後申請鎖lock2,雖然產生了死鎖,但執行緒2後來放棄了鎖lock1申請,並釋放了鎖lock2,所以執行緒1可以得到鎖lock2,輸出執行完成的語句。

限時等待

等待限時是執行緒申請鎖時給定一個等待時間,如果等待時間過後執行緒還沒能拿到鎖,那麼執行緒就停止等待。

像短作業優先排程演算法中有可能出現的飢餓現象,給定鎖一個等待限時是很有用的。想要在申請鎖時給定一個限時,就要用到重入鎖中的方法tryLock(long time,TimeUnit unit),兩個引數,一個表示時長,一個表示時間單位,看下面這段程式碼:

public class TestReentrantLock {
    public static void main(String[] args) {
       ThreadDemo1 threadDemo1 = new ThreadDemo1();
        Thread thread1 = new Thread(threadDemo1,"thread1");
        Thread thread2 = new Thread(threadDemo1,"thread2");

        thread1.start();
        thread2.start();
    }
    static class ThreadDemo1 implements Runnable {
        //例項化一把重入鎖
        public static ReentrantLock lock = new ReentrantLock();

        @Override
        public void run() {
            try {
                //嘗試獲取鎖,拿不到3秒後再試
                if (lock.tryLock(3, TimeUnit.SECONDS)) {
                    System.out.println("Thread " + Thread.currentThread().getId() + " get lock successful.");
                    //拿到鎖後,睡5秒,也就是佔有鎖5秒
                    TimeUnit.SECONDS.sleep(5);
                } else {
                    System.out.println("Thread " + Thread.currentThread().getId() + " get lock failed.");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }
    }
}

對於上面的程式碼,tryLock()方法中等待時長為3,時間單位是秒,也就是申請鎖等待時間最長為3秒。執行緒1獲得鎖lock後會輸出成功語句然後睡眠5秒,此時執行緒2申請鎖lock2就會失敗,在等待5秒後因為還得不到鎖而輸出失敗語句:

Thread 13 get lock successful.
Thread 14 get lock failed.

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