重入鎖ReentrantLock

原文地址:https://www.relaxheart.cn/to/master/blog?uuid=81

靈活的重入鎖


重入鎖ReenterLock我把它理解為是synchronized的增強版,因為重入鎖可以完全替代synchronized關(guān)鍵字。在JDK 5.0的早期版本中,重入鎖的性能遠遠好于synchronized,但從JDK6.0開始對synchronized做了大量的優(yōu)化工作,是的兩者的性能差距并不大了。

重入鎖使用java.util.concurrent.locks.ReentrantLock類來實現(xiàn)。下面是一段最見到那的重入鎖使用案例:

/**
 * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
 * @Date: 2019-5-3 0003 22:43
 * @Description: 重入鎖ReentrantLock的簡單使用案例
 */
public class ReenterLock implements Runnable {

    // 重入鎖
    public static ReentrantLock lock = new ReentrantLock();

    // 共享變量i
    public static int i = 0;

    @Override
    public void run() {
        for (int j = 0; j<1000000; j++){
            // 加鎖
            lock.lock();
            try{
                i++;
            } finally {
                // 釋放鎖
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLock reenterLock = new ReenterLock();
        Thread t1 = new Thread(reenterLock);
        Thread t2 = new Thread(reenterLock);
        t1.start(); t2.start();
        t1.join();  t2.join();
        System.out.println("i = " + i);
    }
}

上述代碼中我們使用了重入鎖保護臨界區(qū)資源i,確保多線程對i操作的安全性。從這段代碼可以看到,與synchronized相比,沖入鎖有著顯示的操作過程。開發(fā)人員必須手動指定何時加鎖,何時釋放鎖。因此重入鎖對邏輯控制的靈活性也遠遠好于synchronized。但值得注意的是,在退出臨界區(qū)時,必須記得釋放鎖,否則其他線程就沒有機會在訪問臨界區(qū)了。

有些同學(xué)可能對重入鎖這“重入”二字有點糊涂,鎖就是鎖嘛,為什么叫重入鎖呢。之所以這么叫是因為這種鎖時可以反復(fù)進入的。當然,這里的反復(fù)僅僅局限于一個線程。我們可以修改上述代碼中加鎖的代碼段:

@Override
    public void run() {
        for (int j = 0; j<1000000; j++){
            // 加鎖
            lock.lock();
            lock.lock();
            try{
                i++;
            } finally {
                // 釋放鎖
                lock.unlock();
                lock.unlock();
            }
        }
    }

在這種情況下一個線程會連續(xù)兩次獲得同一把鎖。如果不允許這么操作,那同一個線程在第二次獲得鎖的時候?qū)妥约寒a(chǎn)生死鎖(線程會卡死在第2次獲取鎖的過程中)。需要注意的是如果一個線程可以多此獲得鎖(即多次lock.lock()),那么相應(yīng)的也要對應(yīng)次數(shù)的釋放鎖(lock.unlock()),相反,如果釋放次數(shù)少了,相當于當前線程還是持有鎖,那其他線程也無法進入臨界區(qū)。

除了上述靈活性外,重入鎖還提供了一些高級功能。比如,重入鎖可以提供終端處理的能力。

中斷響應(yīng)


對于synchronized來說,如果一個線程在等待鎖,那么結(jié)果只有兩種:要么持有鎖,要么保持等待。而使用重入鎖則提供了另外一種可能:那就是線程可以被中斷。也就是在等待鎖的過程中,程序可以根據(jù)需要取消對鎖的請求。有些時候,這么做是非常有必要的。比如:如果你和你的朋友約好一起去打球,如果你等了半小時朋友還沒到,突然接到他的電話有時來不來了,那么你一定就掃興的打道回府或者自己走了。中斷正是提供了一套類似的機制,如果一個線程正在等待鎖,那么它依然可以收到一個通知,被告知無需在等待,可以停止工作了。這種情況對于處理死鎖是有一定幫助的。

下面的代碼產(chǎn)生了一個死鎖,但得益于鎖中斷,我們可以很輕易的解決這個死鎖:

/**
 * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
 * @Date: 2019-5-3 0003 23:05
 * @Description: 重入鎖高級特性:鎖中斷
 */
public class IntLock implements Runnable {

    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    /**
     * 控制加鎖順序,方便構(gòu)造死鎖
     * @param lock
     */
    public IntLock(int lock){
        this.lock = lock;
    }

    @Override
    public void run() {
        try{
            if (lock == 1){
                lock1.lockInterruptibly();
                Thread.sleep(500);
                lock2.lockInterruptibly();
            } else {
                lock2.lockInterruptibly();
                Thread.sleep(500);
                lock1.lockInterruptibly();
            }
        } catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            if (lock1.isHeldByCurrentThread()){
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()){
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getId()+":線程退出");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IntLock intLock1 = new IntLock(1);
        IntLock intLock2 = new IntLock(2);

        Thread t1 = new Thread(intLock1);
        Thread t2 = new Thread(intLock2);

        t1.start(); t2.start();
        Thread.sleep(1000);
        t2.interrupt();

    }
}

線程t1和t2啟動后, t1先占用lock1,在占用lock2; t2先占用lock2,在請求lock1.因此很容易形成t1和t2之間的相互等待。在這里,對鎖的請求,統(tǒng)一使用lockInterruptibly()方法。這是一個可以對中斷進行響應(yīng)的鎖申請動作,即在等待鎖的過程中,可以響應(yīng)中斷。

執(zhí)行結(jié)果:

java.lang.InterruptedException
   at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:944)
   at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1263)
   at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:317)
   at cn.relaxheart.designModel.reenterLock.IntLock.run(IntLock.java:34)
   at java.base/java.lang.Thread.run(Thread.java:835)
14:線程退出
13:線程退出

可以看出,中斷后,兩個線程雙雙退出。但真正完成工作的只有t1.而t2線程則放棄其任務(wù)直接退出,釋放資源。

鎖申請等待時間


除了等待外部通知之外,要避免四速還有另外一種方法,那就是限時等待。依然以約朋友打球為例,如果朋友遲遲不來,又無法聯(lián)系到他。那么,在等待1~2小時后,我想大部分人都會掃興離開。對線程來說也是這樣。通常,我們無法判斷為什么一個線程遲遲拿不到鎖。也許是因為死鎖了,也許是因為產(chǎn)生了饑餓。但如果給定一個等待時間,讓線程自動放棄,那么對系統(tǒng)來說是又意義的。我們可以使用tryLock()方法進行一次顯示等待。
下面代碼展示了限時等待鎖的使用:

/**
 * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
 * @Date: 2019-5-3 0003 23:34
 * @Description:  重入鎖高級特性:限時等待鎖
 */
public class TimeLock implements Runnable {

    public static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try{
            if (lock.tryLock(5, TimeUnit.SECONDS)){
                Thread.sleep(6000);
            } else {
                System.out.println("get lock failed");
            }
        } catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TimeLock timeLock = new TimeLock();
        Thread t1 = new Thread(timeLock);
        Thread t2 = new Thread(timeLock);

        t1.start();
        t2.start();
    }
}

在這里,tryLock()方法接收兩個參數(shù),一個表示等待時長,另外一個表示即使單位。這里的單位設(shè)置為秒,時長為5,表示線程在這個鎖請求中,最多等待5秒。如果超過5秒還沒有得到鎖,就會返回false。如果成功獲得鎖,則返回true。

在這個例子中,由于占用鎖的線程會持有鎖長達6秒,故另一個線程無法在5秒的等待時間內(nèi)獲得鎖,因此,請求鎖會失敗。

ReentrantLock.tryLock()方法也可以不帶參數(shù)直接運行。在這種情況下,當前線程會嘗試獲得鎖,如果鎖并未被其他線程占用,則申請鎖會成功,并立即返回true。如果鎖被其他線程占用,則當前線程不會進行等待,而是立即返回false。這種模式不會引起線程等待,因此也不會產(chǎn)生死鎖。下面演示了這種使用方式:

/**
 * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
 * @Date: 2019-5-3 0003 23:53
 * @Description: 無描述信息
 */
public class TryLock implements Runnable {

    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    public TryLock(int lock){
        this.lock = lock;
    }

    @Override
    public void run() {
        if (lock == 1) {
            while (true){
                if (lock1.tryLock()){
                    try {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {

                        }

                        if (lock2.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + ":My Job done");
                            } finally {
                                lock2.unlock();
                            }
                        }
                    } finally {
                        lock1.unlock();
                    }
                }
            }
        } else {
            while (true){
                if (lock2.tryLock()){
                    try {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {

                        }

                        if (lock1.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + ":My Job done");
                            } finally {
                                lock1.unlock();
                            }
                        }
                    } finally {
                        lock2.unlock();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        TryLock tryLock1 = new TryLock(1);
        TryLock tryLock2 = new TryLock(2);

        Thread t1 = new Thread(tryLock1);
        Thread t2 = new Thread(tryLock2);

        t1.start();
        t2.start();
    }
}

上述代碼中,采用了非常容易死鎖的加鎖順序。也就是先讓t1獲得lock1, 再讓t2獲得lock2,接著做反向請求,讓t1申請lock2, t2申請lock1.在一般情況下,這會導(dǎo)致t1 和 t2相互等待,從而引起死鎖。

但是使用tryLock()后,這種情況就大大改善了。由于線程不會傻傻地等待,而是不停地嘗試,因此,只要執(zhí)行足夠長的時間,線程總是會得到所需要的資源,從而正常執(zhí)行(這里以線程同時獲得lock1,lock2兩把鎖,作為其可以正常執(zhí)行的條件)。在同時獲得lock1 和 lock2后,線程就打印出標志著任務(wù)完成的信息“My Job done”。

執(zhí)行結(jié)果:

13:My Job done
14:My Job done

公平鎖


在大多數(shù)情況下,所得申請都是非公平的。也就是說,線程1首先請求了鎖A,接著線程2也請求了鎖A。那么當鎖A可用時,是線程1可以獲得鎖還是線程2可以獲得鎖呢?這個是不一定的。系統(tǒng)只是會從這個鎖的等待隊列中隨機的挑選一個。因此不能保證其公平性。
而公平的鎖則不會是這樣的,它會按照時間的先后順序,保證先到者先得,后到者后得。公平鎖的一大特點是:它不會產(chǎn)生饑餓現(xiàn)象。只要你排隊,最終還是可以等到資源的。如果我們使用synchronized關(guān)鍵字進行鎖控制,那么產(chǎn)生的鎖就是非公平的。而重入鎖允許我們對其公平性進行設(shè)置。它有一個如下的構(gòu)造函數(shù):

public ReentrantLock (boolean fair)

當參數(shù)fair為true時,表示鎖時公平的。公平鎖看起來很優(yōu)美,但是要實現(xiàn)公平鎖必然要求系統(tǒng)維護一個有序隊列,因此公平鎖的實現(xiàn)成本比較高,性能相對也非常低下,因此,默認情況下,鎖時非公平的。如果沒有特別的需求,也不需要使用公平鎖。公平鎖和非公平鎖在線程調(diào)度上也是又極大差別的。下面代碼可以很好的突出公平鎖的特點:

/**
 * @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
 * @Date: 2019-5-4 0004 0:15
 * @Description: 重入鎖之公平鎖
 */
public class FairLock implements Runnable {

    // 創(chuàng)建一個公平鎖:fairLock
    public static ReentrantLock fairLock = new ReentrantLock(true);

    @Override
    public void run() {
        while (true){
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName() + "獲得鎖");
            } finally {
                fairLock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        FairLock fairLock = new FairLock();
        Thread t1 = new Thread(fairLock, "Thread_t1");
        Thread t2 = new Thread(fairLock, "Thread_t2");

        t1.start();
        t2.start();
    }
}

代碼很簡單,我們直接看運行結(jié)果:


image.png

從結(jié)果可以很明顯的看出兩個線程基本上是交替獲得鎖的,幾乎不會發(fā)生一個線程連續(xù)多此獲得鎖的可能,從而公平性也得到了保障。如果不使用公平鎖,那么情況就完全不一樣了:


image.png

可以看出來根據(jù)系統(tǒng)的調(diào)度,一個線程會傾向于再次獲取已經(jīng)持有的鎖,這種分配方式是高效的,但是無公平性可言。

總結(jié)


一、ReentrantLock的幾個重要方法整理如下:
(1) lock():獲得鎖,如果鎖已經(jīng)被占用,則等待。
(2) lockInterruptibly():獲得鎖,但優(yōu)先響應(yīng)中斷。
(3) tryLock():嘗試獲得鎖,如果成功,返回true,失敗返回false。該方法不等待,立即返回。
(4) tryLock(long time, TimeUnit unit):在給定時間內(nèi)嘗試獲得鎖。
(5) unlock():釋放鎖。

二、就重入鎖的實現(xiàn)來看,它主要幾種在Java層面。在重入鎖的實現(xiàn)中,主要包含三個要素:
(1) 原子狀態(tài)。原子狀態(tài)使用CAS操作來存儲當前鎖的狀態(tài),判斷鎖是否已經(jīng)被別的線程持有。
(2) 等待隊列。所有沒有請求到鎖的線程,會進入等待隊列進行等待。持有線程釋放鎖后,系統(tǒng)就能從等待隊列中喚醒一個線程,繼續(xù)工作。
(3) 阻塞原語park()和unpark(),用來掛起和回復(fù)線程。

三、延申:重入鎖的好搭檔:Condition條件,這里就不說了,感興趣的同學(xué)自己去查一下。

更多博文:https://www.relaxheart.cn/to/master/blog

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。