我們聊一聊可重入鎖特別重要的話題
本文轉(zhuǎn)載自微信公眾號(hào)「三太子敖丙」,作者三太子敖丙 。轉(zhuǎn)載本文請(qǐng)聯(lián)系三太子敖丙公眾號(hào)。
使用Java進(jìn)行多線程開(kāi)發(fā),使用鎖是一個(gè)幾乎不可避免的問(wèn)題。今天,就讓我們來(lái)聊一聊這個(gè)基礎(chǔ),但是又特別特別重要的話題。
首先,讓我們來(lái)看一下,到底什么是鎖? 以及,為什么要使用鎖?
如果有2個(gè)線程,需要訪問(wèn)同一個(gè)對(duì)象User。一個(gè)讀線程,一個(gè)寫線程。User對(duì)象有2個(gè)字段,一個(gè)是名字,一個(gè)是手機(jī)號(hào)碼。
當(dāng)User對(duì)象剛剛創(chuàng)建出來(lái)的時(shí)候,姓名和手機(jī)號(hào)碼都是空。然后,寫線程開(kāi)始填充數(shù)據(jù)。最后,就出現(xiàn)了以下令人心碎的一幕:
可以看到,雖然寫線程先于讀線程工作,但是, 由于寫姓名和寫電話號(hào)碼兩個(gè)操作不是原子的。這就導(dǎo)致讀線程只讀取了半個(gè)數(shù)據(jù),在讀線程看來(lái),User對(duì)象的電話號(hào)碼是不存在。
為了避免類似的問(wèn)題,我們就需要使用鎖。讓寫線程在修改對(duì)象前,先加鎖,然后完成姓名和電話號(hào)碼的賦值,再釋放鎖。而讀線程也是一樣,先取得鎖,再讀,然后釋放鎖。這樣就可以避免發(fā)生這種情況。
如下圖所示:
什么是重入鎖?
好了,現(xiàn)在大家知道我們?yōu)槭裁匆褂面i了。那什么是重入鎖呢。通常情況下,鎖可以用來(lái)控制多線程的訪問(wèn)行為。那對(duì)于同一個(gè)線程,如果連續(xù)兩次對(duì)同一把鎖進(jìn)行l(wèi)ock,會(huì)怎么樣了?對(duì)于一般的鎖來(lái)說(shuō),這個(gè)線程就會(huì)被永遠(yuǎn)卡死在那邊,比如:
- void handle() {
- lock();
- lock(); //和上一個(gè)lock()操作同一個(gè)鎖對(duì)象,那么這里就永遠(yuǎn)等待了
- unlock();
- unlock();
- }
這個(gè)特性相當(dāng)不好用,因?yàn)樵趯?shí)際的開(kāi)發(fā)過(guò)程中,函數(shù)之間的調(diào)用關(guān)系可能錯(cuò)綜復(fù)雜,一個(gè)不小心就可能在多個(gè)不同的函數(shù)中,反復(fù)調(diào)用lock(),這樣的話,線程就自己和自己卡死了。
所以,對(duì)于希望傻瓜式編程的我們來(lái)說(shuō),重入鎖就是用來(lái)解決這個(gè)問(wèn)題的。重入鎖使得同一個(gè)線程可以對(duì)同一把鎖,在不釋放的前提下,反復(fù)加鎖,而不會(huì)導(dǎo)致線程卡死。因此,如果我們使用的是重入鎖,那么上述代碼就 可以正常工作。你唯一需要保證的,就是unlock()的次數(shù)和lock()一樣多。這樣是不是方便很多呢?
Java中的重入鎖
Java中的鎖都來(lái)自與Lock接口,如下圖中紅框內(nèi)的,就是重入鎖。
重入鎖提供的最重要的方法就是lock()
- void lock():加鎖,如果鎖已經(jīng)被別人占用了,就無(wú)限等待。
這個(gè)lock()方法,提供了鎖最基本的功能,拿到鎖就返回,拿不到就等待。因此,大規(guī)模得在復(fù)雜場(chǎng)景中使用,是有可能因此死鎖的。因此,使用這個(gè)方法得非常小心。
如果要預(yù)防可能發(fā)生的死鎖,可以嘗試使用下面這個(gè)方法:
- boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException:嘗試獲取鎖,等待timeout時(shí)間。同時(shí),可以響應(yīng)中斷。
這是一個(gè)比單純lock()更具有工程價(jià)值的方法,如果大家閱讀過(guò)JDK的一些內(nèi)部代碼,就不難發(fā)現(xiàn),tryLock()在JDK內(nèi)部被大量的使用。
與lock()相比,tryLock()至少有下面一個(gè)好處:
- 可以不用進(jìn)行無(wú)限等待。直接打破形成死鎖的條件。如果一段時(shí)間等不到鎖,可以直接放棄,同時(shí)釋放自己已經(jīng)得到的資源。這樣,就可以在很大程度上,避免死鎖的產(chǎn)生。因?yàn)榫€程之間出現(xiàn)了一種謙讓機(jī)制
- 可以在應(yīng)用程序這層進(jìn)行進(jìn)行自旋,你可以自己決定嘗試幾次,或者是放棄。
- 等待鎖的過(guò)程中可以響應(yīng)中斷,如果此時(shí),程序正好收到關(guān)機(jī)信號(hào),中斷就會(huì)觸發(fā),進(jìn)入中斷異常后,線程就可以做一些清理工作,從而防止在終止程序時(shí)出現(xiàn)數(shù)據(jù)寫壞,數(shù)據(jù)丟失等悲催的情況。
當(dāng)然了,當(dāng)鎖使用完后,千萬(wàn)不要忘記把它釋放了。不然,程序可能就會(huì)崩潰啦~
- void unlock() :釋放鎖
此外, 重入鎖還有一個(gè)不帶任何參數(shù)的tryLock()。
- public boolean tryLock()
這個(gè)不帶任何參數(shù)的tryLock()不會(huì)進(jìn)行任何等待,如果能夠獲得鎖,直接返回true,如果獲取失敗,就返回false,特別適合在應(yīng)用層自己對(duì)鎖進(jìn)行管理,在應(yīng)用層進(jìn)行自旋等待。
重入鎖的實(shí)現(xiàn)原理
重入鎖內(nèi)部實(shí)現(xiàn)的主要類如下圖:
重入鎖的核心功能委托給內(nèi)部類Sync實(shí)現(xiàn),并且根據(jù)是否是公平鎖有FairSync和NonfairSync兩種實(shí)現(xiàn)。這是一種典型的策略模式。
實(shí)現(xiàn)重入鎖的方法很簡(jiǎn)單,就是基于一個(gè)狀態(tài)變量state。這個(gè)變量保存在AbstractQueuedSynchronizer對(duì)象中
- private volatile int state;
當(dāng)這個(gè)state==0時(shí),表示鎖是空閑的,大于零表示鎖已經(jīng)被占用, 它的數(shù)值表示當(dāng)前線程重復(fù)占用這個(gè)鎖的次數(shù)。因此,lock()的最簡(jiǎn)單的實(shí)現(xiàn)是:
- final void lock() {
- // compareAndSetState就是對(duì)state進(jìn)行CAS操作,如果修改成功就占用鎖
- if (compareAndSetState(0, 1))
- setExclusiveOwnerThread(Thread.currentThread());
- else
- //如果修改不成功,說(shuō)明別的線程已經(jīng)使用了這個(gè)鎖,那么就可能需要等待
- acquire(1);
下面是acquire() 的實(shí)現(xiàn):
- public final void acquire(int arg) {
- //tryAcquire() 再次嘗試獲取鎖,
- //如果發(fā)現(xiàn)鎖就是當(dāng)前線程占用的,則更新state,表示重復(fù)占用的次數(shù),
- //同時(shí)宣布獲得所成功,這正是重入的關(guān)鍵所在
- if (!tryAcquire(arg) &&
- // 如果獲取失敗,那么就在這里入隊(duì)等待
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
- //如果在等待過(guò)程中 被中斷了,那么重新把中斷標(biāo)志位設(shè)置上
- selfInterrupt();
公平的重入鎖
默認(rèn)情況下,重入鎖是不公平的。什么叫不公平呢。也就是說(shuō),如果有1,2,3,4 這四個(gè)線程,按順序,依次請(qǐng)求鎖。那等鎖可用的時(shí)候,誰(shuí)會(huì)先拿到鎖呢?在非公平情況下,答案是隨機(jī)的。如下圖所示,可能線程3先拿到鎖。
如果你是一個(gè)公平主義者,強(qiáng)烈堅(jiān)持先到先得的話,那么你就需要在構(gòu)造重入鎖的時(shí)候,指定這是一個(gè)公平鎖:
- ReentrantLock fairLock = new ReentrantLock(true);
這樣一來(lái),每一個(gè)請(qǐng)求鎖的線程,都會(huì)乖乖的把自己放入請(qǐng)求隊(duì)列,而不是上來(lái)就進(jìn)行爭(zhēng)搶。但一定要注意,公平鎖是有代價(jià)的。維持公平競(jìng)爭(zhēng)是以犧牲系統(tǒng)性能為代價(jià)的。如果你愿意承擔(dān)這個(gè)損失,公平鎖至少提供了一種普世價(jià)值觀的實(shí)現(xiàn)吧!
那公平鎖和非公平鎖實(shí)現(xiàn)的核心區(qū)別在哪里呢?來(lái)看一下這段lock()的代碼:
- //非公平鎖
- final void lock() {
- //上來(lái)不管三七二十一,直接搶了再說(shuō)
- if (compareAndSetState(0, 1))
- setExclusiveOwnerThread(Thread.currentThread());
- else
- //搶不到,就進(jìn)隊(duì)列慢慢等著
- acquire(1);
- }
- //公平鎖
- final void lock() {
- //直接進(jìn)隊(duì)列等著
- acquire(1);
- }
從上面的代碼中也不難看到,非公平鎖如果第一次爭(zhēng)搶失敗,后面的處理和公平鎖是一樣的,都是進(jìn)入等待隊(duì)列慢慢等。
對(duì)于tryLock()也是非常類似的:
- //非公平鎖
- final boolean nonfairTryAcquire(int acquires) {
- final Thread current = Thread.currentThread();
- int c = getState();
- if (c == 0) {
- //上來(lái)不管三七二十一,直接搶了再說(shuō)
- if (compareAndSetState(0, acquires)) {
- setExclusiveOwnerThread(current);
- return true;
- }
- }
- //如果就是當(dāng)前線程占用了鎖,那么就更新一下state,表示重復(fù)占用鎖的次數(shù)
- //這是“重入”的關(guān)鍵所在
- else if (current == getExclusiveOwnerThread()) {
- //我又來(lái)了哦~~~
- int nextc = c + acquires;
- if (nextc < 0) // overflow
- throw new Error("Maximum lock count exceeded");
- setState(nextc);
- return true;
- }
- return false;
- }
- //公平鎖
- protected final boolean tryAcquire(int acquires) {
- final Thread current = Thread.currentThread();
- int c = getState();
- if (c == 0) {
- //先看看有沒(méi)有別人在等,沒(méi)有人等我才會(huì)去搶,有人在我前面 ,我就不搶啦
- if (!hasQueuedPredecessors() &&
- compareAndSetState(0, acquires)) {
- 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;
- }
Condition
Condition可以理解為重入鎖的伴生對(duì)象。它提供了在重入鎖的基礎(chǔ)上,進(jìn)行等待和通知的機(jī)制??梢允褂? newCondition()方法生成一個(gè)Condition對(duì)象,如下所示。
- private final Lock lock = new ReentrantLock();
- private final Condition condition = lock.newCondition();
那Condition對(duì)象怎么用呢。在JDK內(nèi)部就有一個(gè)很好的例子。讓我們來(lái)看一下ArrayBlockingQueue吧。ArrayBlockingQueue是一個(gè)隊(duì)列,你可以把元素塞入隊(duì)列(enqueue),也可以拿出來(lái)take()。但是有一個(gè)小小的條件,就是如果隊(duì)列是空的,那么take()就需要等待,一直等到有元素了,再返回。那這個(gè)功能,怎么實(shí)現(xiàn)呢?這就可以使用Condition對(duì)象了。
實(shí)現(xiàn)在ArrayBlockingQueue中,就維護(hù)一個(gè)Condition對(duì)象
- lock = new ReentrantLock(fair);
- notEmpty = lock.newCondition();
這個(gè)notEmpty 就是一個(gè)Condition對(duì)象。它用來(lái)通知其他線程,ArrayBlockingQueue是不是空著的。當(dāng)我們需要拿出一個(gè)元素時(shí):
- public E take() throws InterruptedException {
- final ReentrantLock lock = this.lock;
- lock.lockInterruptibly();
- try {
- while (count == 0)
- // 如果隊(duì)列長(zhǎng)度為0,那么就在notEmpty condition上等待了,一直等到有元素進(jìn)來(lái)為止
- // 注意,await()方法,一定是要先獲得condition伴生的那個(gè)lock,才能用的哦
- notEmpty.await();
- //一旦有人通知我隊(duì)列里有東西了,我就彈出一個(gè)返回
- return dequeue();
- } finally {
- lock.unlock();
- }
- }
當(dāng)有元素入隊(duì)時(shí):
- public boolean offer(E e) {
- checkNotNull(e);
- final ReentrantLock lock = this.lock;
- //先拿到鎖,拿到鎖才能操作對(duì)應(yīng)的Condition對(duì)象
- lock.lock();
- try {
- if (count == items.length)
- return false;
- else {
- //入隊(duì)了, 在這個(gè)函數(shù)里,就會(huì)進(jìn)行notEmpty的通知,通知相關(guān)線程,有數(shù)據(jù)準(zhǔn)備好了
- enqueue(e);
- return true;
- }
- } finally {
- //釋放鎖了,等著的那個(gè)線程,現(xiàn)在可以去彈出一個(gè)元素試試了
- lock.unlock();
- }
- }
- private void enqueue(E x) {
- final Object[] items = this.items;
- items[putIndex] = x;
- if (++putIndex == items.length)
- putIndex = 0;
- count++;
- //元素已經(jīng)放好了,通知那個(gè)等著拿東西的人吧
- notEmpty.signal();
- }
因此,整個(gè)流程如圖所示:
重入鎖的使用示例
為了讓大家更好的理解重入鎖的使用方法?,F(xiàn)在我們使用重入鎖,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的計(jì)數(shù)器。這個(gè)計(jì)數(shù)器可以保證在多線程環(huán)境中,統(tǒng)計(jì)數(shù)據(jù)的精確性,請(qǐng)看下面示例代碼:
- public class Counter {
- //重入鎖
- private final Lock lock = new ReentrantLock();
- private int count;
- public void incr() {
- // 訪問(wèn)count時(shí),需要加鎖
- lock.lock();
- try {
- count++;
- } finally {
- lock.unlock();
- }
- }
- public int getCount() {
- //讀取數(shù)據(jù)也需要加鎖,才能保證數(shù)據(jù)的可見(jiàn)性
- lock.lock();
- try {
- return count;
- }finally {
- lock.unlock();
- }
- }
- }
總結(jié)
可重入鎖算是多線程的入門級(jí)別知識(shí)點(diǎn),所以我把他當(dāng)做多線程系列的第一章節(jié),對(duì)于重入鎖,我們需要特別知道幾點(diǎn):
- 對(duì)于同一個(gè)線程,重入鎖允許你反復(fù)獲得通一把鎖,但是,申請(qǐng)和釋放鎖的次數(shù)必須一致。
- 默認(rèn)情況下,重入鎖是非公平的,公平的重入鎖性能差于非公平鎖
- 重入鎖的內(nèi)部實(shí)現(xiàn)是基于CAS操作的。
- 重入鎖的伴生對(duì)象Condition提供了await()和singal()的功能,可以用于線程間消息通信。