java并發(fā)編程(十二)ReentrantLock深入淺出

一、ReentrantLock簡介

1.1 特點(diǎn)

ReentrantLock具有如下的特點(diǎn):

  • 可重入
  • 可中斷
  • 可設(shè)置超時時間
  • 可設(shè)置為公平鎖
  • 支持條件變量

后面會重點(diǎn)講解其特點(diǎn)的實(shí)現(xiàn)原理时迫。

1.2 代碼結(jié)構(gòu)

其代碼結(jié)構(gòu)如下圖:

image.png

有三個內(nèi)部類颅停,分別是:Sync、FairSync掠拳、NonfairSync癞揉。

Sync繼承自AbstractQueuedSynchronizer。

AbstractQueuedSynchronizer當(dāng)中有Node和ConditionObject兩個內(nèi)部類溺欧。

通過上圖類的字面意思應(yīng)該能大概知曉前面提到的特點(diǎn)都是在哪里實(shí)現(xiàn)的了喊熟。

二、原理解析

2.1 可重入

可重入是指同一個線程如果首次獲得了這把鎖姐刁,因?yàn)樗沁@把鎖的擁有者芥牌,因此有權(quán)利再次獲取這把鎖。如果是不可重入鎖聂使,那么第二次嘗試獲得鎖時壁拉,自己也會被鎖擋住。

前面學(xué)習(xí)的synchronized也是可重入鎖柏靶。

可重入使用實(shí)例如下:

public class Test {

    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        method1();
    }
    public static void method1() {
        lock.lock();
        try {
            System.out.println("method1");
            method2();
        } finally {
            lock.unlock();
            System.out.println("method1 unlock");
        }
    }
    public static void method2() {
        lock.lock();
        try {
            System.out.println("method2");
            method3();
        } finally {
            lock.unlock();
            System.out.println("method2 unlock");
        }
    }
    public static void method3() {
        lock.lock();
        try {
            System.out.println("method3");
        } finally {
            lock.unlock();
            System.out.println("method3 unlock");
        }
    }
}

結(jié)果:

method1
method2
method3
method3 unlock
method2 unlock
method1 unlock

需要注意的是扇商,lock.unlock()一定要在finally塊的第一行。

  • 源碼分析

還是使用之前的代碼宿礁,一步步跟蹤:

獲取鎖

    public void lock() {
        sync.lock();
    }

Sync的lock方法有兩個實(shí)現(xiàn)類案铺,公平鎖和非公平鎖:

image.png

此處使用的是非公平鎖,因?yàn)槌跏蓟疪eetrantLock時梆靖,默認(rèn)使用非公平鎖NonfairLock:

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

繼續(xù)源碼跟蹤控汉,非公平鎖當(dāng)中的lock()方法:

        final void lock() {
            // 此處使用自旋鎖,判斷當(dāng)前線程是否持有該鎖返吻,如果是0的話姑子,則將值替換成1
            if (compareAndSetState(0, 1))
                // 上述比較成立,設(shè)置此線程獨(dú)占該鎖
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 比較不成立测僵,嘗試獲取鎖
                acquire(1);
        }

當(dāng)首次執(zhí)行上鎖操作街佑,一定走的的是上面的setExclusiveOwnerThread流程,當(dāng)線程重入或其他線程嘗試獲取該鎖捍靠,走下面的acquire(1):

    public final void acquire(int arg) {
        // 嘗試獲取鎖沐旨,
        if (!tryAcquire(arg) &&
            // 使用短路邏輯運(yùn)算符,當(dāng)獲取失敗榨婆,就繼續(xù)向下走磁携,會將線程添加到等待隊(duì)列當(dāng)中
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 中斷當(dāng)前線程,其實(shí)是設(shè)置中斷標(biāo)記
            selfInterrupt();
    }

我們主要關(guān)注tryAcquire良风,看起如何實(shí)現(xiàn)鎖重入的谊迄,忽略中間過程闷供,直接查看如下代碼:

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 獲取當(dāng)前線程同步狀態(tài),state使用volatile修飾
            int c = getState();
            // 表示沒有線程持有鎖
            if (c == 0) {
                // 自旋加鎖统诺,與前面的過程相同
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 當(dāng)前線程就是持有鎖的線程
            else if (current == getExclusiveOwnerThread()) {
                // 對當(dāng)前狀態(tài) 加 1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 設(shè)置狀態(tài)值
                setState(nextc);
                return true;
            }
            return false;
        }

關(guān)于unlock釋放鎖過程歪脏,直接放關(guān)鍵代碼:

        protected final boolean tryRelease(int releases) {
            // 獲取當(dāng)前狀態(tài) 減1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 鎖重入可以多次,只有當(dāng)狀態(tài)減為0粮呢,才能釋放鎖唾糯。
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

關(guān)于鎖重入的原理比較簡單,就介紹到這里鬼贱。

2.2 可中斷

ReetrantLock處理提供一個常規(guī)的lock()方法之外移怯,還提供了一個可中斷的方法lockInterruptibly(),當(dāng)時用此方法獲取鎖時这难,如果持有鎖的線程發(fā)生中斷舟误,則該方法將拋出異常:

public class InterruptTest {

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("啟動...");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("等鎖的過程中被打斷");
                return;
            }
            try {
                System.out.println("獲得了鎖");
            } finally {
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        System.out.println("獲得了鎖");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(1);
            t1.interrupt();
            System.out.println("執(zhí)行打斷");
        } finally {
            lock.unlock();
        }
    }
}

結(jié)果:

獲得了鎖
啟動...
執(zhí)行打斷
等鎖的過程中被打斷
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.cloud.bssp.juc.reetrantlock.InterruptTest.lambda$main$0(InterruptTest.java:19)
    at java.lang.Thread.run(Thread.java:748)

如果使用的是lock()方法,即使線程發(fā)生中斷姻乓,仍然可以獲取到鎖嵌溢,且不會拋出任何異常。

2.3 可設(shè)置超時時間

ReetrantLock提供了兩個獲取鎖并快速返回的方法蹋岩,不會一直等待赖草,無論成功失敗都將立即返回:

  • tryLock()
    當(dāng)鎖沒有被持有時,即使該鎖是公平鎖剪个,那么tryLock()方法仍會會立即獲得該鎖秧骑,違背公平的原則,但是很有用扣囊。

  • tryLock(long timeout, TimeUnit unit)
    可以設(shè)置超時時間乎折,與tryLock不同的是,此方法在設(shè)置時間結(jié)束時侵歇,會嘗試獲取鎖骂澄,如果成功,則持有鎖并立即返回惕虑,當(dāng)有任何公平原則存在坟冲,且有線程正在等待獲取鎖時,都不能獲取到鎖溃蔫,這與tryLock形成鮮明對比健提。

tryLock()測試:

public class TryLockTest {

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

        Thread t1 = new Thread(() -> {
            if (!lock.tryLock()) {
                System.out.println("獲取鎖失敗");
                return;
            } else {
                try {
                    System.out.println("獲取鎖成功");
                } finally {
                    lock.unlock();
                }
            }

        });

        lock.lock();
        try {
            t1.start();
            TimeUnit.SECONDS.sleep(1);
        } finally {
            lock.unlock();
        }
    }
}

結(jié)果:

獲取鎖失敗

定時tryLock()如下所示:

public class TryLockTimeTest {

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

        Thread t1 = new Thread(() -> {
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    System.out.println("等待一秒獲取鎖失敗");
                    return;
                } else {
                    try {
                        System.out.println("等待一秒獲取鎖成功");
                    } finally {
                        lock.unlock();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        lock.lock();
        try {
            t1.start();
            System.out.println("等待兩秒");
            TimeUnit.SECONDS.sleep(2);
        } finally {
            lock.unlock();
        }
    }
}

結(jié)果:

等待兩秒
等待一秒獲取鎖失敗

2.4 設(shè)置為公平鎖

前面就提到過ReentrantLock 默認(rèn)是不公平的。

之所以使用非公平是因?yàn)楣芥i一般是沒有必要的酒唉,而且會降低并發(fā)度西潘。

使用如下的方式創(chuàng)建公平鎖:

ReentrantLock lock = new ReentrantLock(true);

跟蹤器構(gòu)造器:

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

關(guān)注其公平鎖實(shí)現(xiàn):

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            // 繼承自AQS的方法铐姚,內(nèi)部先調(diào)用tryAcquire獲取鎖财喳,獲取失敗則添加下城到等待隊(duì)列當(dāng)中
            acquire(1);
        }

        /**
         * 公平鎖版本的tryAcquire
         */
        protected final boolean tryAcquire(int acquires) {
            // 獲取當(dāng)前線程
            final Thread current = Thread.currentThread();
            // 獲取鎖的狀態(tài)
            int c = getState();
            // 0表示鎖沒有被持有
            if (c == 0) {
                // 判斷當(dāng)前等待隊(duì)列是否有節(jié)點(diǎn)在等待遭贸,沒有才去競爭
                if (!hasQueuedPredecessors() &&
                    // 比較并替換狀態(tài)
                    compareAndSetState(0, acquires)) {
                    // 設(shè)置當(dāng)前線程為獨(dú)占線程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                // 此處表示鎖重入
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

2.5 條件變量

ReentrantLock支持多個條件變量。

如何理解上面這句話网沾?我們在前面學(xué)習(xí)synchronized時癞蚕,介紹了其wait方法,當(dāng)線程調(diào)用wait方法時辉哥,會從線程的持有者owner變成等待狀態(tài)桦山,會加入到Monitor的WaitSet當(dāng)中,當(dāng)有其他線程再次調(diào)用wait醋旦,仍然會添加進(jìn)來恒水。就好比一個公共的休息室一樣。

而ReentrantLock的多個條件變量就好比成多個休息室饲齐。

ReentrantLock實(shí)現(xiàn)多個條件變量要使用到await()/signal()方法钉凌,以及conditionObject隊(duì)列,后面我們慢慢講解捂人,首先看下其用法:

public class ConditionTest {

    static ReentrantLock lock = new ReentrantLock();

    static Condition Tom = lock.newCondition();

    static Condition Jerry = lock.newCondition();

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

        new Thread(() -> {

            try {
                lock.lock();
                Tom.await();
                System.out.println("吃到了魚");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {

            try {
                lock.lock();
                Jerry.await();
                System.out.println("吃到了奶酪");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();

        TimeUnit.SECONDS.sleep(1);

        try {
            lock.lock();
            System.out.println("魚來了");
            Tom.signal();
        } finally {
            lock.unlock();
        }

        TimeUnit.SECONDS.sleep(1);
        try {
            lock.lock();
            System.out.println("奶酪來了");
            Jerry.signal();
        } finally {
            lock.unlock();
        }
    }
}

結(jié)果:

魚來了
吃到了魚
奶酪來了
吃到了奶酪

如上所示御雕,有幾個重點(diǎn):

  • await前需要獲得鎖
  • await后鎖是被釋放的
  • 調(diào)用signal喚醒線程,但是同樣需要獲取鎖滥搭,否則會報錯酸纲。喚醒后的線程重新競鎖,并且從await后繼續(xù)執(zhí)行瑟匆。
  • lock后一定記得unlock闽坡。

下面我們重點(diǎn)關(guān)注下是如何實(shí)現(xiàn)的?只講解重點(diǎn)方法

await方法:

        public final void await() throws InterruptedException {
            // 如果線程狀態(tài)是中斷愁溜,則拋出異常
            if (Thread.interrupted())
                throw new InterruptedException();
            // 將當(dāng)前線程加入條件等待隊(duì)列
            Node node = addConditionWaiter();
            // 釋放鎖的占用
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 當(dāng)節(jié)點(diǎn)不在同步等待隊(duì)列時
            while (!isOnSyncQueue(node)) {
                // 阻塞當(dāng)前線程
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 獲取等待隊(duì)列的鎖 并且不拋出中斷異常
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                // 重新設(shè)置中斷標(biāo)記
                interruptMode = REINTERRUPT;
            // 清除取消的節(jié)點(diǎn)
            if (node.nextWaiter != null) 
                unlinkCancelledWaiters();
            // 如果中斷模式不是0无午,則根據(jù)狀態(tài)決定拋出異常,中斷線程還是什么都不執(zhí)行
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

addConditionWaiter方法:

        private Node addConditionWaiter() {
            // 當(dāng)前conditionObject當(dāng)中的最后一個等待者
            Node t = lastWaiter;
            // 如果最后一個等待者被取消祝谚,請清空(不是null宪迟,且狀態(tài)不是等待)
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 將當(dāng)前線程設(shè)置為等待狀態(tài)的節(jié)點(diǎn)
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

signal方法:

        public final void signal() {
            // 判斷線程是否持有了鎖,沒有則拋出異常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            //如果條件隊(duì)列的第一等待者不是null交惯,執(zhí)行信號喚醒
            if (first != null)
                doSignal(first);
        }

doSignal方法

        private void doSignal(Node first) {
            do {
                如果第一個節(jié)點(diǎn)的下一個等待者是null
                if ( (firstWaiter = first.nextWaiter) == null)
                     //則條件隊(duì)列的最后一個等待者設(shè)置為null
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

transferForSignal方法

    final boolean transferForSignal(Node node) {
        // 比較節(jié)點(diǎn)狀態(tài)是否是condition次泽,是則更新成0,否則返回false
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * 添加節(jié)點(diǎn)到同步等待隊(duì)列
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        // 此處等待狀態(tài)是0席爽,比較并替換狀態(tài)為SIGNAL
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            // 解除線程阻塞狀態(tài)
            LockSupport.unpark(node.thread);
        return true;
    }

關(guān)于ReentrantLock就簡單介紹這些了意荤,其實(shí)應(yīng)該先學(xué)習(xí)AQS的,不然可能不太理解源碼只锻。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末玖像,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子齐饮,更是在濱河造成了極大的恐慌捐寥,老刑警劉巖笤昨,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異握恳,居然都是意外死亡瞒窒,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門乡洼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來崇裁,“玉大人,你說我怎么就攤上這事束昵“挝龋” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵锹雏,是天一觀的道長壳炎。 經(jīng)常有香客問我,道長逼侦,這世上最難降的妖魔是什么匿辩? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮榛丢,結(jié)果婚禮上铲球,老公的妹妹穿的比我還像新娘。我一直安慰自己晰赞,他們只是感情好稼病,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掖鱼,像睡著了一般然走。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上戏挡,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天芍瑞,我揣著相機(jī)與錄音,去河邊找鬼褐墅。 笑死拆檬,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的妥凳。 我是一名探鬼主播竟贯,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼逝钥!你這毒婦竟也來了屑那?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎持际,沒想到半個月后沃琅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡选酗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年阵难,在試婚紗的時候發(fā)現(xiàn)自己被綠了岳枷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芒填。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖空繁,靈堂內(nèi)的尸體忽然破棺而出殿衰,到底是詐尸還是另有隱情,我是刑警寧澤盛泡,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布闷祥,位于F島的核電站,受9級特大地震影響傲诵,放射性物質(zhì)發(fā)生泄漏凯砍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一拴竹、第九天 我趴在偏房一處隱蔽的房頂上張望悟衩。 院中可真熱鬧,春花似錦栓拜、人聲如沸座泳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挑势。三九已至,卻和暖如春啦鸣,著一層夾襖步出監(jiān)牢的瞬間潮饱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工诫给, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饼齿,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓蝙搔,卻偏偏與公主長得像缕溉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子吃型,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容