05.鎖機制和條件對象簡述

Java的鎖機制主要分內(nèi)置鎖(隱式鎖)和顯式鎖烈涮。

內(nèi)置鎖

Java每個對象都有一個內(nèi)置的鎖對象朴肺,這些鎖對象不需要顯式地創(chuàng)建,可以直接對所有的Java對象加鎖坚洽,所以也叫隱式鎖戈稿。

雖然鎖對象是存在的,但是只有對鎖對象加鎖才能保證同步性讶舰。從現(xiàn)實角度看這是很容易理解的鞍盗,你擁有一把門鎖,但是你沒有鎖上绘雁,別人依舊能推開門橡疼。

內(nèi)置鎖的加鎖機制在JVM指令層面是通過monitorEnter和monitorExit來實現(xiàn)的,在操作系統(tǒng)層使用的是管程來實現(xiàn)互斥以及使用條件對象庐舟,在語言層面使用的是synchronized關(guān)鍵字欣除,使用synchronized聲明的方法,實例方法是對實例對象加鎖挪略,靜態(tài)方法是對class對象加鎖历帚,synchronized塊可以指定加鎖的對象滔岳,實現(xiàn)比同步方法更加細粒度的加鎖操作。

進入synchronized代碼塊即獲取了鎖挽牢,退出代碼塊(正常谱煤、異常)即釋放了鎖,這些操作是隱式的禽拔。

重入

獲取了鎖對象之后還能再次獲取同一個鎖刘离,說明這個鎖是可重入的《闷埽“重入”以為著鎖的操作粒度是線程而非調(diào)用硫惕。

重入的一種實現(xiàn)方法是為每個鎖關(guān)聯(lián)一個獲取計數(shù)值和一個所有者線程。計數(shù)值為0時野来,說明鎖沒有被線程持有恼除,當線程請求一個未被占用的鎖,JVM會記下鎖的持有者曼氛,并給鎖的計數(shù)器遞增一豁辉,如果同一個線程再次獲取這個鎖(未釋放鎖其他線程獲取不了),計數(shù)值自增舀患,釋放是遞減徽级,直到計數(shù)值為0,則線程釋放了鎖构舟。也就是說灰追,可重入鎖的釋放與重入要對應(yīng)堵幽。

顯式鎖

Java5.0新增了一個新的鎖機制:ReentrantLock(可重入鎖)狗超。這是一種顯式鎖,即需要顯式地創(chuàng)建對象的鎖對象朴下。

可重入鎖不是取代內(nèi)置鎖的努咐,它是在內(nèi)置鎖不適用時,作為一種高級功能使用的鎖殴胧。

Lock和ReentrantLock

ReentrantLock是Lock接口的實現(xiàn)渗稍。

Lock接口提供了一種無條件、可輪詢团滥、定時以及可中斷的鎖獲取操作竿屹,所有加鎖和解鎖操作都是顯式的。

ReentrantLock實現(xiàn)了Lock接口灸姊,并提供了與synchronized相同的互斥性和可見性拱燃。lock.lock()獲取鎖,和進入synchronized代碼塊有相同的內(nèi)存語義力惯,lock.unlock()釋放鎖碗誉,和離開synchronized代碼塊有相同的內(nèi)存語義召嘶。

非阻塞的鎖獲取

tryLock()方法提供了可定時和可輪詢的鎖獲取方法,可以有效地避免死鎖的發(fā)生哮缺。

可中斷的鎖獲取

lockInterruptibly方法在獲取鎖阻塞時可以被中斷弄跌,被中斷時會拋出InterruptedException。

非塊結(jié)構(gòu)的加鎖

鎖分段尝苇,使用多個鎖保護一個大的數(shù)據(jù)結(jié)構(gòu)铛只,使得鎖的粒度更小,降低鎖的競爭糠溜。內(nèi)置鎖也支持鎖分段格仲。

公平性

在ReentrantLock的構(gòu)造器可以指定公平性的設(shè)置,默認是非公平鎖诵冒。公平鎖的性能沒有非公平鎖高凯肋。

讀寫鎖ReadWriteLock

讀寫鎖內(nèi)部有兩個鎖對象,讀鎖和寫鎖汽馋,讀鎖共享寫鎖獨占侮东。

在讀寫鎖的加鎖策略中,允許多個讀同時執(zhí)行豹芯,但是同一時刻只能有一個寫悄雅。

ReentrantReadWriteLock

可重入讀寫鎖提供了可重入的加鎖語義。當訪問以讀取為主的數(shù)據(jù)結(jié)構(gòu)時铁蹈,讀寫鎖能提高伸縮性宽闲。

在公平的鎖中,如果一個鎖由讀線程持有握牧,二另一個線程請求寫入鎖容诬,那么其他讀線程都不能獲取讀鎖,知道寫線程使用完釋放了寫鎖沿腰。在非公平的鎖中览徒,線程獲取訪問許可的順序是不定的。寫線程降級為讀線程是允許的颂龙,但是反過來不可以习蓬,讀線程可以同時運行,如果同時請求寫鎖措嵌,容易造成死鎖躲叼。

public class ReadWriteMap<K,V>{
    private final Map<K,V> map;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock r = lock.readLock();
    private final Lock w = lock.writeLock();
    
    public ReadWriteMap(Map<K,V> map){
        this.map = map;
    }
    
    public V put(K key,V value){
        w.lock();
        try{
            return map.put(key,value);
        }finally{
            w.unlock();
        }
        
    }
    public V get(Object key){
        r.lock();// 避免讀-寫沖突憨攒,能和寫鎖互斥
        try{
            return map.get(key);
        }finally{
            r.unlock();
        }
    }
}

鎖優(yōu)化

JVM為高效并發(fā)做出的鎖優(yōu)化技術(shù)赘被,為了在多線程之間更加高效地共享數(shù)據(jù)锹雏,以及減少數(shù)據(jù)競爭問題脸狸,從而提高程序執(zhí)行效率躬窜。
主要技術(shù)有:

  • 適應(yīng)性自旋
  • 鎖清除
  • 鎖粗化
  • 輕量級鎖
  • 偏向鎖

自旋鎖和適應(yīng)性自旋

互斥同步對性能最大的影響是阻塞屏镊,阻塞會導(dǎo)致線程被掛起速妖,掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)來進行妹蔽。

為了讓線程等待鎖的時候不被操作系統(tǒng)掛起,讓線程執(zhí)行一個忙循環(huán)神帅,即自旋再姑。如果等待的鎖很快就被釋放了,那么自旋是很有意義的找御,但是如果一直沒有等到鎖釋放元镀,這段占用時間片的無實際意義的自旋操作只是白白浪費資源而已。

自適應(yīng)自旋是JVM會根據(jù)最近的等待時間自適應(yīng)地去安排自旋的時間霎桅。

鎖清除

鎖清除是編譯器的優(yōu)化措施栖疑,當JIT在運行時發(fā)現(xiàn)某個加鎖的操作只會被一個線程執(zhí)行,就會清除這個鎖滔驶。減少開銷遇革。

鎖粗化

如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖操作粗化到整個操作外層揭糕。

輕量級鎖

輕量級鎖是相對于使用系統(tǒng)信號量實現(xiàn)的傳統(tǒng)鎖而言的萝快。

輕量級鎖的用途是在沒有多線程競爭的前提下,減少傳統(tǒng)鎖的性能消耗著角。

對象模型

Java對象保存在堆內(nèi)存中揪漩。在內(nèi)存中,一個Java對象包含三部分:對象頭吏口、實例數(shù)據(jù)和對齊填充奄容。其中對象頭是一個很關(guān)鍵的部分,因為對象頭中包含鎖狀態(tài)標志产徊、線程持有的鎖等標志昂勒。

HotSpot虛擬機的對象頭分為兩部分信息,一部分保存對象自身的運行時數(shù)據(jù)囚痴,如hashcode叁怪、GC分代年齡等审葬,這部分數(shù)據(jù)長度在32位虛擬機是32位的64位則是64位深滚,官方稱之為Mark Word。這是實現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵涣觉。

另一部分存儲的是指向方法區(qū)的對象類型數(shù)據(jù)的指針痴荐,如果是數(shù)組對象的話,還會有一個額外的部分用于存儲數(shù)據(jù)長度官册。
在32位的Hotspot虛擬機中對象未被鎖定的情況下生兆,mark word的32位空間中25位存儲hashcode,4位存儲對象分代年齡,2位存儲鎖標志位鸦难,1為固定0根吁,在其他狀態(tài)下mw存儲的內(nèi)容如下表:

table-Hotspot虛擬機對象頭Mark Word

存儲內(nèi)容 標志位 狀態(tài)
hashcode和分代年齡 01 未鎖定
指向鎖記錄的指針 00 輕量級鎖定
指向重量級鎖的指針 10 重量級鎖定
11 Gc標記
偏向線程ID、偏向時間戳合蔽、對象分代年齡 01 可偏向

輕量級鎖獲取執(zhí)行邏輯:

  1. 成功進入同步塊時(未鎖定)击敌,鎖標志位是01,虛擬機將在棧幀中建立一個叫鎖記錄(lock record)的空間拴事,存儲MarkWord的副本沃斤,叫做displaced mark word.
  2. jvm使用cas(compareAndSwitch)操作來嘗試將對象的MarkWord更新為指向lock record的指針。如果更新操作成功刃宵,說明當前線程成功獲取了鎖衡瓶,且對象的鎖標志位變成00。如果更新操作失敗了牲证,jvm會檢查對象的Mark Word是否指向當前線程的棧幀哮针,如果是則說明當前線程以及擁有了對象鎖(這個應(yīng)該發(fā)生在重入時),否則說明這個鎖被其他線程搶占了坦袍。
  3. 如果有兩條線程搶一個鎖诚撵,那輕量級鎖就不再有效了,會膨脹成重量級鎖键闺,鎖標志位變成10寿烟,后面等待鎖的過程也要變成阻塞狀態(tài)。
    輕量級鎖解鎖邏輯:
  4. 判斷對象的MW是否仍指向線程的鎖記錄辛燥,是就用CAS操作把對象當前的MW和lock record中的displaced MW替換回來筛武,替換成功就釋放鎖了。如果替換失敗了挎塌,說明有其他線程嘗試獲取該鎖徘六,就要在釋放鎖的同時,喚醒被掛起的線程榴都。

流程圖:java輕量級鎖原理.note

輕量級鎖是使用cas去代替?zhèn)鹘y(tǒng)的操作信號量的來同步的方式待锈。它能夠提升性能的前提是在同步周期沒有線程競爭鎖,實際上對于大部分鎖都是這個情況嘴高,這是一個經(jīng)驗數(shù)據(jù)竿音。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷拴驮,如果存在競爭春瞬,除了互斥量開銷還有CAS開銷,那就會更加慢套啤。

CAS 是處理器提供的非阻塞原子更新操作指令宽气。

偏向鎖

偏向鎖的目的是消除數(shù)據(jù)在無競爭情況下的 同步原語,進而提高性能。

輕量級鎖是避開使用OS的互斥量萄涯,偏向鎖是把整個鎖去除掉绪氛。偏向鎖的偏向是偏向第一個獲取它的線程,如果在接下來的執(zhí)行過程中涝影,該鎖沒有被其他線程獲取钞楼,則持有偏向鎖的線程將不需要再進行同步。

條件對象

有些情況下袄琳,在進入某個鎖的臨界區(qū)后询件,需要滿足一些條件才能繼續(xù)執(zhí)行,于是主動釋放鎖唆樊,且阻塞直到被通知再被喚醒宛琅。

條件對象是基于鎖機制上的一種協(xié)作機制,不滿足條件時在主動阻塞直到鎖條件對象發(fā)出通知才會被喚醒繼續(xù)執(zhí)行逗旁。
條件對象主要有內(nèi)置條件對象和顯式條件對象嘿辟,內(nèi)置條件對象時內(nèi)置鎖的條件對象,一個內(nèi)置鎖對象只能有一個條件對象片效;顯式條件對象可以支持多個條件對象红伦。

內(nèi)置條件對象

正如每個Java對象都有一個內(nèi)置的鎖對象,都可以作為一個鎖淀衣,每個對象同樣可以作為一個條件對象昙读。

Object類的wait()/wait(timeout)/wait(timeout,nanos),notify()和notifyAll()構(gòu)成內(nèi)置條件對象的API。

public class Object{
    //...
    public final native void notify();
    public final native void notifyAll();
    public final void wait() throws InterruptedException{//...}
    public final native void wait(long timeout) throws InterruptedException;
    public final void wait(long timeout, int nanos) throws InterruptedException{
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
        if (nanos > 0) {
            timeout++;
        }
        wait(timeout);
    }
}

對象的內(nèi)置鎖和其內(nèi)部條件對象時相互關(guān)聯(lián)的膨桥,要調(diào)用對象X中條件隊列的任何一個方法蛮浑,必須持有X上的鎖。簡單的說只嚣,調(diào)用條件對象的方法沮稚,必須在此對象的鎖保護的同步塊內(nèi)部。

需要注意的是册舞,wait()方法會釋放當前持有的鎖,阻塞當前線程蕴掏,并等待直到超時(如果設(shè)置了超時,超時后會自己醒來)调鲸,如何線程被中斷(wait中的線程可被Interrupt且會拋出異常)或者被一個通知喚醒盛杰。喚醒線程后,線程從wait處繼續(xù)執(zhí)行(當然作為就緒態(tài)線程和其他線程一起競爭鎖)线得。

notify從等待條件的線程隊列中隨機喚醒一個饶唤,notifyAll是喚醒隊列中所有的。一般常用All贯钩,因為只喚醒一個可能會造成死鎖。

顯式條件對象

當對于一個鎖等待不同的條件時,內(nèi)置鎖就無法實現(xiàn)了角雷。顯式條件對象Condition支持多個條件祸穷。
Condition的定義:

public interface Condition{
    void await() throws Interruptedexception;
    boolean await(long timeout,TimeUnit unit) throws Interruptedexception;
    long awaitNanos(long nanostimeout) throws Interruptedexception;
    void awaitUninterruptibly();
    boolean awaitUntil(Date deadline) throws Interruptedexception;
    void singal();
    void singalAll();
}

使用顯式條件變量的有界緩存

public class ConditionBoundedBuffer<T>{
    protected final Lock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    
    private final T[] items = (T[])new Object[BUFFER_SIZE];
    private int tail,head,count;
    //阻塞直到notFull
    public void put(T x) throws Interruptedexception{
        lock.lock();
        try{
            while(count == items.length)
                notFull.await();
            item[tail] = x;
            if(++tail == item.length)
                tail=0;
            ++count;
            notEmpty.singalAll();
        }finally{
            lock.unlock();
        }
    }
    
    // 阻塞直到notEmpty
    public T take() throws Interruptedexception{
        lock.lock();
        try{
            while(count==0)
                notEmpty.await();
            T x  = item[head];
            if(++head == items.length)
                head=0;
            --count;
            notFull.singalAll();
            return x;
        }finally{
            lock.unlock();
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市勺三,隨后出現(xiàn)的幾起案子雷滚,更是在濱河造成了極大的恐慌,老刑警劉巖吗坚,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祈远,死亡現(xiàn)場離奇詭異,居然都是意外死亡商源,警方通過查閱死者的電腦和手機车份,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牡彻,“玉大人扫沼,你說我怎么就攤上這事∽穑” “怎么了缎除?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長总寻。 經(jīng)常有香客問我器罐,道長,這世上最難降的妖魔是什么渐行? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任技矮,我火速辦了婚禮,結(jié)果婚禮上殊轴,老公的妹妹穿的比我還像新娘衰倦。我一直安慰自己,他們只是感情好旁理,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布樊零。 她就那樣靜靜地躺著,像睡著了一般孽文。 火紅的嫁衣襯著肌膚如雪驻襟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天芋哭,我揣著相機與錄音沉衣,去河邊找鬼。 笑死减牺,一個胖子當著我的面吹牛豌习,可吹牛的內(nèi)容都是我干的存谎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼肥隆,長吁一口氣:“原來是場噩夢啊……” “哼既荚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起栋艳,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤恰聘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吸占,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體晴叨,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年矾屯,在試婚紗的時候發(fā)現(xiàn)自己被綠了兼蕊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡问拘,死狀恐怖遍略,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情骤坐,我是刑警寧澤绪杏,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站纽绍,受9級特大地震影響蕾久,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拌夏,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一僧著、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧障簿,春花似錦盹愚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至西篓,卻和暖如春愈腾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背岂津。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工虱黄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吮成。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓橱乱,卻偏偏與公主長得像辜梳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子仅醇,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

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