【Java并發(fā)編程】淺析Lock接口和AQS(AbstractQueuedSynchronizer)

概述

在學習java并發(fā)編程的過程中侨把,java.util.concurrent包是我們需要學習和理解的關鍵之一,concurrent包的作者Doug Lea在其中為我們提供了大量實用铛漓,高性能的工具愕贡。在并發(fā)編程的過程中潦嘶,這些工具也得到了廣泛的應用创南。

Concurrent包的層次結構

打開java.util.concurrent包伦忠,我們會發(fā)現其中包含了兩個子包atomiclocks,另外還有很多非阻塞隊列和ThreadPoolExecutor稿辙,而這些就是concurrent包的精華昆码。后面我也會對其中的關鍵類的源碼進行逐個研讀,增加自己對并發(fā)編程的理解邻储。

concurrent包的結構.png

分析整個concurrent包赋咽,得出如下concurrent整體實現示意圖,如下:

concurrent整體實現示意圖.png

圖片來自http://www.reibang.com/p/7a65ab32de2a

Lock接口

鎖是用來控制多線程訪問共享資源的方式芥备,一般在說冬耿,一個鎖能防止多個線程同時訪問共享資源(但是也有些鎖允許多個線程同時訪問共享資源,例如讀寫鎖)萌壳。在Lock接口出現之前,java程序靠synchronized關鍵字來實現鎖功能日月,在Java SE5之后袱瓮,并發(fā)包新增了Lock接口和相關實現類用來實現鎖功能,它提供了與synchronized關鍵字相似的功能爱咬,只是在使用時需要顯示的獲取鎖和釋放鎖尺借。雖然它失去了synchronized提供的隱式獲取和釋放鎖的便捷性,但是卻擁有了鎖獲取和釋放的可操作性精拟、可中斷獲取鎖以及超時獲取鎖等多種synchronized不具備的特性燎斩。(摘自:《Java并發(fā)編程的藝術》)

Lock的使用也非常簡單,通常我們顯示的使用鎖方式如下:

ReentrantLock reentrantLock = new ReentrantLock();
//獲取鎖
reentrantLock.lock();
try {
    //業(yè)務邏輯處理
    ...
}catch (Exception e){
    //異常捕獲
    ...
}finally {
    //釋放鎖
    reentrantLock.unlock();
}

這里需要注意的是蜂绎,一定要在finally中釋放鎖栅表,目的是保證在獲取到鎖之后,最終能夠被釋放师枣。

Lock的API

接下來我們來看下Lock接口中提供的幾種方法:


Lock接口提供的方法.png

對這幾種方法的解釋怪瓶,從源碼的注釋中翻譯過來大概可以做出以下總結:

方法名稱 描述
void lock() 獲取鎖,當前線程獲取到鎖后返回
void lockInterruptibly() 可中斷的獲取鎖践美,該方法會響應中斷洗贰,即在鎖的獲取過程中可以中斷線程
boolean tryLock() 非阻塞的獲取鎖找岖,如果獲取到鎖了返回true,否則返回false
boolean tryLock(long,TimeUnit) 超時獲取鎖敛滋,在超時時間內或未中斷的情況下能夠獲取到鎖许布,超時后會返回false
void unLock() 釋放鎖
Condition newCondition() 獲取與lock綁定的等待通知組件,當前線程必須獲得了鎖才能進行等待绎晃,進行等待時會先釋放鎖蜜唾,當再次獲取鎖時才能從等待中返回

下面我們來簡單看下實現了Lock接口的類是怎樣來實現這五個方法的,以ReentrantLock為例箕昭,可以看下摘出來的部分源碼:

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer{...}

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

    public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
    }

    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    ...

從上面的源碼可以發(fā)現灵妨,ReentrantLock本身對Lock接口的5個方法實現都非常簡單,其核心都是調用了一個繼承于AbstractQueuedSynchronizer(AQS)的類落竹。所以想要理解Lock接口這五個方法的詳細實現泌霍,那么理解AQS就非常必要了。

初識AQS

AQS又稱隊列同步器述召,它是用來構建鎖或者其他同步組件的基礎框架朱转,它使用了一個int state成員變量來表示同步狀態(tài),通過內置的FIFO隊列來完成資源獲取線程的排隊工作。AQS的主要使用方法是繼承积暖,子類通過繼承AQS并實現它的抽象方法來管理同步狀態(tài)藤为,而對狀態(tài)的修改,AQS提供了3個方法:getState(),setState(int newState)compareAndSetState(int expect,int update)夺刑,這三個方法可以保證對狀態(tài)的修改是安全的缅疟。

AQS的子類推薦被定義為自定義同步組件的靜態(tài)內部類,同步器自身沒有實現任何同步接口遍愿,它僅僅是定義了若干同步狀態(tài)獲取和釋放的方法來供自定義同步組件使用存淫,同步器既可以支持獨占式的獲取同步狀態(tài),也可以支持共享式的獲取同步狀態(tài)沼填,這樣就可以方便的實現不同類型的同步組件(ReentrantLock桅咆、ReentrantReadWriteLockCountDownLatch等)。

同步器是實現鎖(或任意同步組件)的關鍵坞笙,在鎖的實現中聚合同步器岩饼,利用同步器實現鎖的語義⊙σ梗可以這樣理解二者的關系:鎖是面向使用者的籍茧,它定義了使用者與鎖交互的接口,隱藏了實現細節(jié)却邓;同步器面向的是鎖的實現者硕糊,它簡化了鎖的實現方式,屏蔽同步狀態(tài)管理、線程的排隊简十、等待和喚醒等底層操作檬某。鎖和同步器很好的隔離了使用者和實現者鎖關注的領域。

AQS的接口與示例

AQS同步器的設計是基于模板方法模式的,使用者需要繼承同步器并重寫指定的方法螟蝙,隨后將同步器組合在自定義同步組件中恢恼,并調用同步器提供的模板方法,而這些方法又會調用使用者重寫的方法胰默。這里可能有點繞场斑,我們還是以ReentrantLock為例,摘取部分源碼來看下

AQS中需要重寫tryAcquire()方法:

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

在ReentrantLock類中繼承自AQS的內部類NonfairSync是這樣實現tryAcquire的:

static final class NonfairSync extends Sync {
        
        ......

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

abstract static class Sync extends AbstractQueuedSynchronizer {

        ......        

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        ......

在AQS模板方法acquire()中調用了子類實現的tryAcquire()方法:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

當繼承AQS的NonfairSync調用模板方法acquire時就會調用已經被NonfairSync重寫的tryAcquire方法,這就是使用AQS的方式牵署。在重寫同步器指定的方法時漏隐,需要使用下面這三個方法來訪問或修改同步狀態(tài):

方法名 描述
getState() 獲取當前同步狀態(tài)
setState(int newState) 設置當前同步狀態(tài)
compareAndSetState(int expect,int new State) 使用CAS設置當前狀態(tài),該方法能夠保證設置狀態(tài)的原子性

AQS可重寫的方法和描述奴迅,我從《Java并發(fā)編程的藝術》一書中摘取過來如下:


AQS可重寫的方法和描述.png
AQS可重寫的方法和描述2.png

在實現自定義同步組件時青责, 將會調用同步器提供的模板方法,這些模板方法和描述如下:


繼承AQS調用的模板方法.png

同步器提供的模板方法基本上可以分為以下三類:

  1. 獨占式獲取和釋放同步狀態(tài)取具;
  2. 共享式獲取和釋放同步狀態(tài)脖隶;
  3. 查詢同步隊列中的線程等待情況。

只有掌握了同步器的工作原理暇检,才能更深入的理解并發(fā)包中其他的并發(fā)組件产阱,下面寫一個例子來看看。


public class Mutex implements Lock{

        private final Sync sync = new Sync();

        @Override
        public void lock() {
                sync.acquire(1);
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {
                sync.acquireInterruptibly(1);
        }

        @Override
        public boolean tryLock() {
            return sync.tryAcquire(1);
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return sync.tryAcquireNanos(1,unit.toNanos(time));
        }

        @Override
        public void unlock() {
                sync.release(0);
        }

        @Override
        public Condition newCondition() {
            return sync.newCondition();
        }

        private static class Sync extends  AbstractQueuedSynchronizer{
                @Override
                protected boolean tryAcquire(int arg) {
                        if(compareAndSetState(0,1)){
                                setExclusiveOwnerThread(Thread.currentThread());
                                return true;
                        }
                        return false;
                }

                @Override
                protected boolean tryRelease(int arg) {
                        if(getState() == 0) {throw new IllegalStateException();}
                        setExclusiveOwnerThread(null);
                        setState(0);
                        return true;
                }

                @Override
                protected boolean isHeldExclusively() {
                        return getState() == 1;
                }

                final ConditionObject newCondition() {
                        return new ConditionObject();
                }
        }


        public static void main(String[] args) {
                Mutex mutex = new Mutex();
                for (int i = 0 ; i< 10 ; i ++){
                        Thread thread = new Thread(){
                                @Override
                                public void run(){
                                        System.out.println("線程:"+Thread.currentThread().getName()+ " 創(chuàng)建成功块仆!");
//                                        if(mutex.tryLock()){
//                                                try {
//                                                        System.out.println("線程: "+Thread.currentThread().getName()+" 獲取到鎖构蹬!");
//                                                        Thread.sleep(3*1000);
//                                                } catch (InterruptedException e) {
//                                                        e.printStackTrace();
//                                                } finally {
//                                                        System.out.println("線程: "+Thread.currentThread().getName()+" 釋放鎖!");
//                                                        mutex.unlock();
//                                                }
//                                        }
                                        try {
                                                mutex.lock();
                                                System.out.println("線程: "+Thread.currentThread().getName()+" 獲取到鎖悔据!");
                                                Thread.sleep(3*1000);
                                        } catch (InterruptedException e) {
                                                e.printStackTrace();
                                        } finally {
                                                System.out.println("線程: "+Thread.currentThread().getName()+" 釋放鎖怎燥!");
                                                mutex.unlock();
                                        }
                                }
                        };
                        thread.start();
                }
        }

}

執(zhí)行情況如下圖:

AQS的DEMO運行截圖.png

上面的例子中,獨占鎖Mutex是一個自定義的同步組件蜜暑,它在同一時刻只允許一個線程占有鎖。Mutex中定義了一個靜態(tài)內部類策肝,該內部類繼承了同步器并實現了獨占式獲取和釋放同步狀態(tài)的方法肛捍。在main方法中,我用for循環(huán)創(chuàng)建了10個線程之众,run方法中有兩種獲取鎖的方式拙毫,mutex.lock()和mutex.tryLock()。查看程序執(zhí)行情況棺禾,可以發(fā)現缀蹄,當Thread-8獲取到獨占鎖時,其余線程都處于等待狀態(tài)(阻塞中)。如果執(zhí)行tryLock()方法話缺前,當Thread-8獲取到線程后,其余線程獲取不到鎖會蛀醉,會直接結束。從這個例子中看到衅码,Mutex在對state狀態(tài)的修改時拯刁,調用了getState(),setState(),compareAndSetState()方法,在lock()方法中逝段,只需要調用AQS的模板方法acquire()垛玻。

可以發(fā)現實現一個自定義同步器,主要是利用AQS奶躯,AQS屏蔽了同步狀態(tài)修改帚桩、線程排隊等底層實現,大大降低了實現一個可靠自定義同步組件的門檻嘹黔。

后面我會對AQS的源碼進行更深層次的學習账嚎,來提升自己對同步器的理解。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末参淹,一起剝皮案震驚了整個濱河市醉锄,隨后出現的幾起案子,更是在濱河造成了極大的恐慌浙值,老刑警劉巖恳不,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異开呐,居然都是意外死亡烟勋,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門筐付,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卵惦,“玉大人,你說我怎么就攤上這事瓦戚【谀颍” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵较解,是天一觀的道長畜疾。 經常有香客問我,道長印衔,這世上最難降的妖魔是什么啡捶? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮奸焙,結果婚禮上瞎暑,老公的妹妹穿的比我還像新娘彤敛。我一直安慰自己,他們只是感情好了赌,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布墨榄。 她就那樣靜靜地躺著,像睡著了一般揍拆。 火紅的嫁衣襯著肌膚如雪渠概。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天嫂拴,我揣著相機與錄音播揪,去河邊找鬼。 笑死筒狠,一個胖子當著我的面吹牛猪狈,可吹牛的內容都是我干的。 我是一名探鬼主播辩恼,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼雇庙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了灶伊?” 一聲冷哼從身側響起疆前,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎聘萨,沒想到半個月后竹椒,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡米辐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年胸完,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翘贮。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡赊窥,死狀恐怖,靈堂內的尸體忽然破棺而出狸页,到底是詐尸還是另有隱情锨能,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布芍耘,位于F島的核電站腹侣,受9級特大地震影響,放射性物質發(fā)生泄漏齿穗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一饺律、第九天 我趴在偏房一處隱蔽的房頂上張望窃页。 院中可真熱鬧跺株,春花似錦、人聲如沸脖卖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽畦木。三九已至袖扛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間十籍,已是汗流浹背蛆封。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留勾栗,地道東北人惨篱。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像围俘,于是被迫代替她去往敵國和親砸讳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345