ReentrantLock重入鎖

一、ReentrantLock重入鎖

1、ReentrantLock重入鎖簡(jiǎn)介

ReentrantLock可以完全替代synchronized關(guān)鍵字。在JDK5.0的早期版本中,ReentrantLock的性能遠(yuǎn)遠(yuǎn)好于synchronized,但是兇JDK6.0開始synchronized上做了大量?jī)?yōu)化,使得兩者性能差距并不大。

重入鎖使用java.until.concurrent.locks.ReentrantLock類來實(shí)現(xiàn)。ReenterLock.java展示了簡(jiǎn)單的.ReentrantLock使用案例。

public class ReenterLock implements Runnable
{
    public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;

    @Override
    public void run()
    {
        for (int j = 0; j < 10000000; ++j)
        {
            lock.lock();    //對(duì)臨界區(qū)加鎖
            try
            {
                ++i;
            }
            finally
            {
                lock.unlock(); //退出臨界區(qū)時(shí)必須釋放鎖
            }
        }
    }

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

從上面的代碼可以看到,與synchronized相比,ReentrantLock有著顯式的操作過程。開發(fā)人員必須手動(dòng)指定何時(shí)加鎖,何時(shí)釋放鎖。因此ReentrantLock對(duì)邏輯控制的靈活性遠(yuǎn)好于synchronized。但是必須注意,在退出臨界區(qū)時(shí),一定要釋放鎖。否則其他線程就沒機(jī)會(huì)再訪問臨界區(qū)了。

ReentrantLock之所以叫重入鎖,是因?yàn)檫@種鎖是可以反復(fù)進(jìn)入的。當(dāng)然,這里的反復(fù)僅僅局限于一個(gè)線程。
如下面代碼展示:

            lock.lock();    //對(duì)臨界區(qū)加鎖
            lock.lock();
            try
            {
                ++i;
            }
            finally
            {
                lock.unlock(); //退出臨界區(qū)時(shí)必須釋放鎖
                lock.unlock();
            }

在這種情況下,一個(gè)線程連續(xù)兩次獲得通一把鎖,這是允許的,如果不允許的話,那么第二個(gè)線程在第2次獲得鎖時(shí),將會(huì)和自己產(chǎn)生死鎖。但是值得注意的是,如果一個(gè)線程多次獲得鎖,那么在釋放鎖的時(shí)候,也必須釋放相同的次數(shù),如果釋放鎖的次數(shù)多,那么就會(huì)得到一個(gè)java.lang.IllegalMonitorStateException異常,反之,如果釋放的鎖次數(shù)少了,那么相當(dāng)于線程還持有這個(gè)鎖,其他線程也無法進(jìn)入臨界區(qū)。

2、中斷響應(yīng)

當(dāng)使用synchronized來對(duì)臨界區(qū)加鎖,如果一個(gè)線程在等待鎖,那么結(jié)果只有兩種情況,要么它獲得鎖繼續(xù)執(zhí)行,要么它就保持等待。但是如果使用重入鎖,就會(huì)提供另一種選擇,那就是線程可以被中斷。也就是說在等待鎖的過程中,程序可以根據(jù)需要取消對(duì)鎖的請(qǐng)求

下面的代碼IntLock.java會(huì)產(chǎn)生一個(gè)死鎖,但是得益于鎖中斷,我們可以很輕易的解決這個(gè)死鎖。

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();
                try
                {
                    Thread.sleep(500);
                }
                catch (InterruptedException e) {}
                lock2.lockInterruptibly();
            }
            else
            {
                lock2.lockInterruptibly();
                try
                {
                    Thread.sleep(500);
                }
                catch (InterruptedException e) {}
                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 r1 = new IntLock(1);
        IntLock r2 = new IntLock(2);
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);

        //中斷其中一個(gè)線程
        t2.interrupt();
    }
}

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

當(dāng)代碼執(zhí)行到Thread.sleep(1000)時(shí),主線程處于休眠,此時(shí),這兩個(gè)線程處于死鎖的狀態(tài),當(dāng)執(zhí)行到t2.interrupt()時(shí),由于t2被中斷,故t2會(huì)放棄對(duì)lock1的申請(qǐng),同時(shí)釋放已獲得的lock2,這個(gè)操作導(dǎo)致t1線程可以順利得到lock2而繼續(xù)執(zhí)行下去。

執(zhí)行上述代碼,可以看到以下輸出:


image.png

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

3、鎖申請(qǐng)等待限時(shí)

除了等待外部通知之外,要避免死鎖還有另外一種方法,那就是限時(shí)等待。對(duì)于線程來說,通常我們無法判斷為什么一個(gè)線程遲遲拿不到鎖。也許是因?yàn)殒i死了,也許因?yàn)楫a(chǎn)生了饑餓。但如果給定一個(gè)等待時(shí)間,讓線程自動(dòng)放棄,那么對(duì)系統(tǒng)來說是有意義的。我們可以使用tryLock()方法進(jìn)行一次限時(shí)等待。

下面代碼TimeLock.java展示了限時(shí)等待鎖的使用

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 tl = new TimeLock();
        Thread t1 = new Thread(tl);
        Thread t2 = new Thread(tl);

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

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

在本例中,由于占用鎖的線程會(huì)持有鎖長(zhǎng)達(dá)6秒,故另一個(gè)線程無法再5秒的等待時(shí)間內(nèi)獲得鎖,因此,請(qǐng)求會(huì)失敗。

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

4、公平鎖

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

public ReentrantLock(boolean fair)

當(dāng)參數(shù)fair為true時(shí),表示鎖時(shí)公平的。但是實(shí)現(xiàn)公平鎖必須要犧牲一定的性能,因?yàn)樾枰S護(hù)一個(gè)有序的隊(duì)列,因此默認(rèn)情況下,鎖是非公平的。如果沒有特別的要求,也不需要使用公平鎖。下面的代碼可以很好的突出公平鎖的特點(diǎn):

public class FairLock implements Runnable
{
    //當(dāng)參數(shù)fair為true時(shí),表示鎖是公平的。默認(rèn)為false,非公平
    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 r1 = new FairLock();
        Thread t1 = new Thread(r1, "Thread_t1");
        Thread t2 = new Thread(r1, "Thread_t2");
        t1.start();
        t2.start();
    }
}

執(zhí)行代碼,會(huì)看到如下輸出結(jié)果:


因?yàn)榇a會(huì)產(chǎn)生大量輸出,這里只截取部分進(jìn)行說明。在這個(gè)輸出中,很明顯可以看到,兩個(gè)線程基本上是交替獲得鎖的,幾乎不會(huì)發(fā)生一個(gè)線程連續(xù)多次獲得鎖的可能,從而公平性也得到了保證。但是如果使用不公平鎖,那么情況就會(huì)不一樣,部分輸出如下:


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

5、總結(jié)

(1)ReentrantLock 幾個(gè)重要方法
  • lock():獲得鎖,如果已經(jīng)被占用,則等待。
  • lockInterruptibly():獲得鎖,但優(yōu)先響應(yīng)中斷。
  • tryLock():嘗試獲得鎖,如果成功,返回true,失敗返回false。該方法不等待,立即返回。
  • tryLock(long time, TimeUnit unit):在給定時(shí)間內(nèi)嘗試獲得鎖。
  • unlock():釋放鎖。
(2)重入鎖實(shí)現(xiàn)中包含的三要素
  • 原子狀態(tài):原子狀態(tài)使用CAS操作來存儲(chǔ)當(dāng)前鎖的狀態(tài),判斷鎖是否已經(jīng)被別的線程持有。
  • 等待隊(duì)列:所有沒有請(qǐng)求到鎖的線程,會(huì)進(jìn)入等待隊(duì)列進(jìn)行等待,待有線程釋放鎖后,系統(tǒng)就能從等待隊(duì)列中喚醒一個(gè)線程,繼續(xù)工作。
  • 阻塞原語park()和unpark(),用來掛起和恢復(fù)線程。沒有得到鎖的線程會(huì)被掛起。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • reentrantLock 、 condition 是 JAVA 1.6 時(shí)推出的,也是用來實(shí)現(xiàn)多線程同步的,和 ...
    前行的烏龜閱讀 4,099評(píng)論 0 2
  • 1. 線程安全 多個(gè)線程對(duì)公共資源進(jìn)行非原子操作,就會(huì)存在線程安全問題 多線程環(huán)境多個(gè)線程共享一個(gè)資源對(duì)資源進(jìn)行非...
    洋蔥520閱讀 692評(píng)論 0 0
  • 一、ReentrantLock 我們都知道鎖是為了保護(hù)臨界區(qū)資源被多線程并發(fā)訪問時(shí)的安全性。 而 Reentran...
    barry_di閱讀 419評(píng)論 0 1
  • 在開發(fā)Java多線程應(yīng)用程序中,各個(gè)線程之間由于要共享資源,必須用到鎖機(jī)制。Java提供了多種多線程鎖機(jī)制的實(shí)現(xiàn)方...
    千淘萬漉閱讀 7,106評(píng)論 1 33
  • 舌尖上的美食,父親的香腸臘肉,是一種叫做時(shí)光的味道 家鄉(xiāng)樂山群山環(huán)繞,綠水長(zhǎng)流,在我們這個(gè)南方小城里,流傳著一種健...
    周云浩閱讀 1,543評(píng)論 3 4

友情鏈接更多精彩內(nèi)容