題記:本文會盡量模擬面試現(xiàn)場對話情景掸犬, 用口語而非書面語 ,采用問答形式來展現(xiàn)绪爸。另外每一個問題都附上“延伸”湾碎,這部分內(nèi)容是幫助小伙伴們更深的理解一些底層細(xì)節(jié)的補充,在面試中可能很少直接涉及奠货,權(quán)當(dāng)是提高自身水平的知識儲備吧介褥。
一、Java基礎(chǔ)
1. 問:List 和 Set 都有什么區(qū)別?
分析:這種問題面試官一般想考察的都是你對這兩種數(shù)據(jù)結(jié)構(gòu)的了解柔滔,已經(jīng)使用時候的選擇依據(jù)溢陪,所以可以從數(shù)據(jù)結(jié)構(gòu)和一些使用案例入手分別做個介紹
答:List,列表睛廊,元素可重復(fù)功氨。常用的實現(xiàn)ArrayList和LinkedList浅乔,前者是數(shù)組方式來實現(xiàn)窒篱,后者是通過鏈表來實現(xiàn)穷绵,在使用選擇的時候,一般考慮的是基本數(shù)據(jù)結(jié)構(gòu)的特性嘶朱,比如蛾坯,數(shù)組讀取效率較高,鏈表插入時效率較高疏遏。
Set脉课,集合,特點就是存儲的元素不可重復(fù)改览。常用是實現(xiàn)下翎,是HashSet和TreeSet,分開來談:
HashSet宝当,底層實現(xiàn)是HashMap视事,存儲時,把值存在key庆揩,而value統(tǒng)一存儲一個object對象俐东。排重的時候,是先通過對象的hashcode來判斷订晌,如果不相等虏辫,直接存儲。如果相等锈拨,會再對key做equals判斷砌庄,如果依然相等,不存儲奕枢,如果不相等娄昆,則存入,我們知道缝彬,HashMap是數(shù)組+鏈表的基本結(jié)構(gòu)萌焰,同樣的,在HashSet中谷浅,也是通過同樣的策略扒俯,存儲在相同的數(shù)組位置下的鏈表中奶卓。
TreeSet,存入自定義對象時撼玄,對象需要實現(xiàn)Comparable接口夺姑,重寫排序規(guī)則。使用場景一般是需要保證存儲數(shù)據(jù)的有序和唯一性互纯。底層數(shù)據(jù)結(jié)構(gòu)是自平衡的二叉排序樹(紅黑樹)
延伸:
- 在說到HashMap時瑟幕,可能會直接引到HashMap相關(guān)話題,我發(fā)現(xiàn)這個問題面試官非常喜歡問留潦,可能是因為HashMap可聊的較多,也能很好的考驗下應(yīng)聘者對底層實現(xiàn)細(xì)節(jié)辣往、源碼閱讀兔院、刨根問底的態(tài)度。
- 涉及到二叉樹了站削,小端就遇到過一次問紅黑樹特性的坊萝,因為之前準(zhǔn)備過,胸有成竹的啪啪啪正要一一道來呢许起,結(jié)果剛說到第二個特性十偶,面試官就問:紅黑樹和普通的平衡二叉樹有什么區(qū)別?當(dāng)時一臉懵逼樣...回來后趕緊補足园细,核心區(qū)別就是:紅黑樹也是二叉查找樹的一種惦积,二叉樹需要通過自旋、或其他方式(比如紅黑樹還能通過變色)來保證平衡(否則就成了鏈表結(jié)構(gòu)了猛频,沒有時間復(fù)雜度上的優(yōu)勢了)狮崩,紅黑樹限定的條件相對來說比較寬松,也就是說在平衡的過程中鹿寻,消耗相對較小睦柴。
- 由于HashSet無序,為了實現(xiàn)有序的目的毡熏,又不想用其他數(shù)據(jù)結(jié)構(gòu)坦敌,可以用LinkedHashSet。簡要說明痢法,同HashSet和HashMap關(guān)系一樣狱窘,也是使用了一個LinkedHashMap,LinkedHashMap和普通的HashMap的區(qū)別就是疯暑,在原有數(shù)據(jù)結(jié)構(gòu)之上训柴,采用雙向鏈表的形式將所有entry(注意,是前面講過的數(shù)組+鏈表中的各個鏈表里的元素妇拯,做了連接)連起來幻馁,順序就是entry的插入順序洗鸵,這樣可以保證元素的迭代順序和插入順序相同(有序性)
2. HashMap 是線程安全的嗎,為什么不是線程安全的?
分析:這種問題仗嗦,既然面試官問起膘滨,肯定是在這方面有的聊的。這個問題稀拐,在多數(shù)情況下火邓,能清晰、全面的描述出問題來龍去脈即可德撬,很少有面試官真的拿一段源碼來考铲咨。當(dāng)然,作為應(yīng)聘者蜓洪,如果能理解的很透徹纤勒,再用簡單的圖邊寫邊講,作為補充隆檀,還是非常出彩的摇天。
答:嗯,不是線程安全的恐仑。主要從兩個方面來說:
- 在插入新數(shù)據(jù)的時候泉坐,多線程hash后的結(jié)果相同,插入位置也就會定位到數(shù)組的相同下標(biāo)下的同一個鏈表中裳仆。在插入時腕让,假如第二個線程在第一個線程插入的瞬間也插入,就可能會導(dǎo)致鉴逞,覆蓋前面插入的值记某。
- 第二個非線程安全的影響是在擴容的時候,擴容會把所有值重新hash构捡,插入到新的擴容后的“數(shù)組+鏈表”結(jié)構(gòu)中液南。可能會導(dǎo)致環(huán)狀鏈表情況出現(xiàn)勾徽,這樣在讀取節(jié)點的next節(jié)點時滑凉,永不為空,也就是著名的擴容時CPU使用率100%情況喘帚。
延伸:
要做到心中有底畅姊,還是需要知其然知其所以然才行,所以吹由,擼下源碼好好想想若未,才能做到臨陣不亂。
- 建議好好看看源碼倾鲫,源碼量不大粗合,結(jié)構(gòu)也比較清晰萍嬉。
- 針對環(huán)狀鏈表的情況,我是看了別人寫的圖文并茂版的文章弄明白的隙疚,這里放上鏈接壤追,供參考:HashMap高并發(fā)問題
3. 問:那你是用什么數(shù)據(jù)結(jié)構(gòu)來作為替代,滿足線程安全的場景要求呢供屉?
分析:這里一般面試官想考察的是對ConcurrentHashMap的了解行冰,但是也有個別情況,會涉及到HashTable伶丐,小端就吃過這方面的虧悼做,明明可以一筆帶過的小知識點,卻由于準(zhǔn)備不充分撵割,而沒能答完整贿堰。
答:在Java里,提供了以下數(shù)據(jù)結(jié)構(gòu)啡彬,可以解決線程安全問題:
- HashTable,實現(xiàn)原理是通過Synchronize同步鎖來把讀寫方法都加鎖的方式故硅。雖然線程安全了庶灿,但是效率低下,只要有讀寫吃衅,就不能做其他操作往踢。
- SynchronizedMap(涉及較少,了解即可)徘层,有條件的同步峻呕,是Collections類提供的一個方法返回的HashMap的多線程版本。實現(xiàn)是把基本的方法都加了同步鎖趣效。
- ConcurrentHashMap(重頭戲):根據(jù)jdk版本不同瘦癌,實現(xiàn)也有差別。
java8以前跷敬,是用segment(鎖住map一段實現(xiàn)讯私,默認(rèn)是16段也就是可以并發(fā)數(shù)支持到16,也可自定義西傀。讀不受影響)斤寇,數(shù)據(jù)結(jié)構(gòu)為數(shù)組(segment)+數(shù)組(entry)+鏈表,適用于讀多寫少的場景拥褂。提供原子操作putIfAbsent()(沒有則添加)娘锁。segment繼承自ReentrantLock,來作為鎖饺鹃。
java8莫秆,元素結(jié)構(gòu)為Node(實現(xiàn)Entry接口)间雀,數(shù)據(jù)結(jié)構(gòu)為數(shù)組+鏈表;直接對Node進(jìn)行加鎖馏锡,粒度更小雷蹂。當(dāng)鏈表長度大于8,轉(zhuǎn)換為紅黑樹,當(dāng)然在轉(zhuǎn)換前杯道,先看下數(shù)組長度匪煌,如果小于64,那先通過擴容來實現(xiàn)党巾;插入元素時萎庭,如果該位置為null,用CAS插入齿拂;如果不為null驳规,則加Synchronize鎖插入到鏈表;
擴展:
- 我們看到署海,數(shù)組的默認(rèn)長度都是16吗购,那么這個數(shù)字有什么深意嗎?有砸狞!做運算時效率高捻勉!屬于取巧的設(shè)計。一個是擴容的時候刀森,直接向左位操作踱启,移一位,擴張為二倍研底;二是hash取模的時候埠偿,hash值是32位,右移28位榜晦,剩高四位冠蒋,然后與這個length-1也就是15(1111)按位與操作,使數(shù)據(jù)均勻分布芽隆。
- ConcurrentHashMap在獲取size時候是怎么計算的呢浊服?
1.7size(),先獲取segment的大小,然后判斷是否修改過胚吁,如果是牙躺,在加鎖重新獲取segment大小,然后把所有segment大小加在一起腕扶;
1.8size()的實現(xiàn)是用一個volatile 變量來做CAS修改孽拷,如果高并發(fā),還會把部分值存到一個volatile數(shù)組半抱。取值時脓恕,把這兩部分的值加在一起膜宋。mappingcount()方法和size()方法實現(xiàn)方式一樣
- hashmap可以使用null作為key和value,存儲于table數(shù)組第一個節(jié)點炼幔;hashtable不允許key和value為null.(這個在一個小公司被面試官問倒了秋茫,汗顏)
- 了解一些其他的數(shù)據(jù)結(jié)構(gòu):
- ConcurrentSkipListMap 數(shù)據(jù)有序
- ConcurrentSkipListSet 能去重
- hashset是用hashmap實現(xiàn),內(nèi)部存的是key乃秀,所有的value都是同一個object
二肛著、ThreadLocal
1. 問:請談?wù)勀銓hreadLocal的理解。
分析:在多線程環(huán)境下跺讯,我們經(jīng)常遇到這樣的場景:維護(hù)一個全局變量枢贿。如果要保證變量值的正確性(或者說變量值修改的原子性),需用什么方式來實現(xiàn)呢刀脏?是的局荚,對修改代碼加鎖可以實現(xiàn),保證了在同一時刻只有一個線程來修改該變量值愈污。辦法當(dāng)然不止一種耀态,并發(fā)包AtomicXXX一樣能達(dá)到這個效果,原理暂雹,差不多茫陆,無非是通過鎖來實現(xiàn)并發(fā)。那么還有沒有其他思路呢擎析?有,ThreadLocal挥下,實現(xiàn)思路可謂是另辟蹊徑揍魂。
答:每個線程,都會有一個Map(ThreadLocalMap)棚瘟,用來存儲以我們定義的ThreadLocal對象為key现斋,以我們自定義的值為value的 名值對。而這個Map偎蘸,是來自于我們寫的多線程程序繼承的父線程Thread庄蹋。以此機制,保證了多線程間該變量值的隔離迷雪。
看下源碼限书,以get()方法為切入口:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
重點是第三行,當(dāng)前線程作為參數(shù)傳入章咧,我們來看下getMap(t)做了什么倦西?
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
是的,拿到當(dāng)前線程對象的threadLocals對象赁严,我們可以通過方法返回值推斷扰柠,是一個ThreadLocalMap類型的對象粉铐。那么這個對象在哪定義的呢?繼續(xù)看源碼:
public class Thread implements Runnable {
......
ThreadLocal.ThreadLocalMap threadLocals = null;
......
}
很明顯卤档,是在Thread類里定義蝙泼。
擴展:內(nèi)存泄漏問題。
ThreadLocal對象是弱引用劝枣。在GC時汤踏,會直接回收。這種情況下哨免,Map中的key為null茎活,value值還在,無法得到及時的釋放琢唾。目前的策略是在調(diào)用get载荔、set、remove等方法時采桃,會啟動回收這些值懒熙。但是如果一直沒調(diào)用呢?嗯普办,很容易就導(dǎo)致內(nèi)存泄漏了工扎。當(dāng)然,并不能因為此就認(rèn)為是弱引用導(dǎo)致的內(nèi)存泄露衔蹲,而應(yīng)該是肢娘,設(shè)計的這個變量存儲機制,導(dǎo)致了泄露舆驶。所以在使用的時候橱健,要及時釋放(通過以上描述,你肯定已經(jīng)想到怎么合理釋放了吧沙廉?)
三拘荡、Valotile
1. 問:請你說下對Valotile的了解,以及使用場景撬陵。
分析:多線程編程珊皿,我們要解決的問題集中在三個方面:
- 原子性:最簡單的例子就是,i++,在多線程環(huán)境下巨税,最終的結(jié)果是不確定的蟋定,為什么?就是因為這么一個++操作垢夹,被編譯為指令 后溢吻,是多個指令來完成的。那么遇到并發(fā)的情況,就會導(dǎo)致彼此“覆蓋”的情況促王。
- 可見性:通俗解釋就是犀盟,在A線程對一個變量做了修改,在B線程中蝇狼,能正確的讀取到修改后的結(jié)果阅畴。究其原理,是cpu不是直 接 和系統(tǒng)內(nèi)存通信迅耘,而是把變量讀取到L1贱枣,L2等內(nèi)部的緩存中,也叫作私有的數(shù)據(jù)工作棧颤专。修改也是在內(nèi)部緩存中纽哥,但是何時同步到系統(tǒng)內(nèi)存是不能確定的,有了這個時間差栖秕,在并發(fā)的時候春塌,就可能會導(dǎo)致,讀到的值簇捍,不是最新值只壳。
- 有序性:這里只說指令重排序,虛擬機在把代碼編譯為指令后執(zhí)行暑塑,出于優(yōu)化的目的吼句,在保證結(jié)果不變的情況下,可能會調(diào)整指 令的執(zhí)行順序事格。
答:valotile惕艳,能滿足上述的可見性和有序性。但是無法保證原子性驹愚。
- 可見性尔艇,是在修改后,強制把對變量的修改同步到系統(tǒng)內(nèi)存么鹤。而其他cpu在讀取自己的內(nèi)部緩存中的值的時候,發(fā)現(xiàn)是valotile修飾 的味廊,會把內(nèi)部緩存中的值蒸甜,置為無效,然后從系統(tǒng)內(nèi)存讀取余佛。
- 有序性柠新,是通過內(nèi)存屏障來實現(xiàn)的。所謂的內(nèi)存屏障辉巡,可以理解為恨憎,在某些指令中,插入屏障指令,用以確保憔恳,在向屏障指令后面 繼續(xù)執(zhí)行的時候瓤荔,其前面的所有指令已經(jīng)執(zhí)行完畢。
擴展:在寫單例模式時钥组,我們通常會采用雙層判斷的方式输硝,在最內(nèi)層:
instance = new Singleton()
其實這也有一個隱含的問題:這句賦值語句,其實是分三步來操作的:
- 為instance分配內(nèi)存
- 調(diào)用Singleto構(gòu)造函數(shù)來初始化變量
- instance指向上一步初始化的對象
在jvm做了指令重排序優(yōu)化后程梦,上述步驟b和c不能保證点把,可能出現(xiàn),c先執(zhí)行屿附,但是對象卻沒初始化郎逃,這時候其他線程判斷的時候,發(fā)現(xiàn)是非null挺份,但是使用的時候褒翰,卻沒有具體實例,導(dǎo)致報錯压恒。所以影暴,我們可以用valotile來修飾instance,避免該問題探赫。
四型宙、多線程——Synchronized
1. 問:你平時涉及到多線程編程多不多?談?wù)勀銓ynchronized鎖的理解
分析:多從實現(xiàn)原理伦吠,工作機制來描述
答:在多線程編程中妆兑,為了達(dá)到線程安全的目的,我們往往通過加鎖的方式來實現(xiàn)毛仪。而Synchronized正是java提供給我們的非常重要的鎖之一搁嗓。它屬于jvm級別加鎖,底層實現(xiàn)是:在編譯過程中箱靴,在指令級別加入一些標(biāo)識來實現(xiàn)的腺逛。例如,同步代碼塊衡怀,會在同步代碼塊首尾加入monitorenter和monitorexit字節(jié)碼指令棍矛,這兩個指令都需要一個reference類型的參數(shù),指明要加鎖和解鎖的對象抛杨,同步方法則是通過在修飾符上加acc_synchronized標(biāo)識實現(xiàn)够委。在執(zhí)行到這些指令(標(biāo)識)時,本質(zhì)都是獲取怖现、占有茁帽、釋放monitor鎖對象實現(xiàn)互斥,也就是說,同一時刻潘拨,只能有一個線程能成功的獲取到這個鎖對象吊输。我們看一段加了synchronized關(guān)鍵字的代碼編譯后的字節(jié)碼。編譯前:
public class test {
public test() {
}
public static void main(String[] args) {
synchronized(new Object()){
int i = 0;
}
}
}
編譯后:
public class test extends java.lang.Object{
public test();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."":()V
4: nop
5: return
public static void main(java.lang.String[]);
Code:
0: new #2; //class Object
3: dup
4: invokespecial #1; //Method java/lang/Object."":()V
7: dup
8: astore_1
9: monitorenter // Enter the monitor associated with object
10: iconst_0
11: istore_2
12: nop
13: aload_1
14: monitorexit // Exit the monitor associated with object
15: goto 23
18: astore_3
19: aload_1
20: monitorexit // Be sure to exit monitor...
21: aload_3
22: athrow
23: nop
24: return
Exception table:
from to target type
15 18 any
21 18 any
}
重點關(guān)注14行战秋,和20行璧亚。
在使用Synchronized時,用到的方法是wait和notify(或notifyAll)脂信,他們的原理是癣蟋,調(diào)用wait方法后,線程讓出cpu資源狰闪,釋放鎖疯搅,進(jìn)入waiting狀態(tài),進(jìn)入等待隊列【第一個隊列】埋泵。當(dāng)有其他線程調(diào)用了notify或者notifyAll喚醒時幔欧,會將等待隊列里的線程對象,移入阻塞隊列【第二個隊列】丽声,狀態(tài)是blocked礁蔗,等待鎖被釋放后(這個釋放時機,由虛擬機來決定雁社,人為無法干預(yù))浴井,開始競爭鎖。
Synchronized無法中斷正在阻塞隊列或者等待隊列的線程霉撵。
擴展:Synchronized提供了以下幾種類型的鎖:偏向鎖磺浙、輕量級鎖、重量級鎖徒坡。在大部分情況下撕氧,并不存在多線程競爭,更常見的是一個線程多次獲取同一個鎖喇完。那么很多的消耗伦泥,其實是在鎖的獲取與釋放上。Synchronized一直在被優(yōu)化锦溪,可以說Synchronized雖然推出的較早奄喂,但是效率并不比后來推出的Lock差。
偏向鎖:在jdk1.6中引入海洼,目的是消除在無競爭情況下的同步原語(翻譯成人話就是,即使加了synchronized關(guān)鍵字富腊,但是在沒有競爭的時候坏逢,沒必要去做獲取-持有-釋放鎖對象的操作,提高程序運行性能)。怎么做呢是整?當(dāng)鎖對象第一次被線程A獲取時肖揣,虛擬機會把對象頭中的標(biāo)志位設(shè)置為01,也就是代表偏向模式浮入。同時把代表這個線程A的ID龙优,通過CAS方式,更新到鎖對象頭的MarkWord中事秀。相同的線程下次再次申請鎖的時候彤断,只需要簡單的比較線程ID即可。以上操作成功易迹,則成功進(jìn)入到同步代碼塊宰衙。如果此時有其他線程B來競爭該鎖,分兩種情況做不同的處理:
- 如果線程A已執(zhí)行完(并不會主動的修改鎖對象的狀態(tài))睹欲,會直接撤銷鎖對象的狀態(tài)為未鎖定供炼,標(biāo)志位為00;
- 如果線程A還在持有該鎖窘疮,則鎖升級為輕量級鎖袋哼。
輕量級鎖:也是JDK1.6中引入的,輕量級闸衫,是相對于使用互斥量的重量級鎖來說的涛贯。線程發(fā)生競爭鎖的時候,不會直接進(jìn)入阻塞狀態(tài)楚堤,而是先嘗試做CAS修改操作疫蔓,進(jìn)入自旋,這個過程避免了線程狀態(tài)切換的開銷身冬,不過要消耗cpu資源衅胀。詳細(xì)過程是:
- 線程嘗試進(jìn)入同步代碼塊,如果鎖對象未被鎖定酥筝,在當(dāng)前線程對應(yīng)的棧幀中滚躯,建立鎖記錄的空間,用于存儲鎖對象Mark Word的拷貝嘿歌。
- 然后JVM用CAS方式嘗試將鎖對象的MarkWord內(nèi)容替換為指向前述“鎖記錄”的指針掸掏。如果成功,當(dāng)前線程則持有了鎖宙帝,處于輕量級鎖定狀態(tài)丧凤;如果失敗,會首先檢查當(dāng)前MarkWord是否已經(jīng)指向當(dāng)前線程棧幀的“鎖記錄”步脓,如果是愿待,就說明當(dāng)前線程已經(jīng)擁有了這個鎖浩螺,直接重入即可。否則就表明是其他線程持有鎖仍侥,那么進(jìn)入自旋(其實就是重試CAS修改操作)要出。
- 釋放鎖時,是使用CAS來講MarkWord和“鎖記錄”里的內(nèi)容互換农渊。如果成功患蹂,成功釋放;如果事變砸紊,表明當(dāng)前鎖存在競爭(被其他線程修改了MarkWord里的數(shù)據(jù))传于,此時,鎖會升級為重量級鎖批糟。
重量級鎖:也就是我們使用的互斥量方式實現(xiàn)的鎖格了,當(dāng)存在多線程競爭時,只要沒拿到鎖徽鼎,就會進(jìn)入阻塞狀態(tài)盛末,主要消耗是在阻塞-喚起-阻塞-喚起的線程狀態(tài)切換上。
上面介紹的三種類型的鎖否淤,是JVM來負(fù)責(zé)管理使用哪種類型鎖悄但,以及鎖的升級(注意,沒有降級)石抡。
五檐嚣、多線程——Lock
1. 問:你平時涉及到多線程編程多不多?談?wù)勀銓ock鎖的理解
分析:最好對比著synchronized來講
答:
在多線程編程中啰扛,為了達(dá)到線程安全的目的嚎京,我們往往通過加鎖的方式來實現(xiàn)。Lock鎖是java代碼級別來實現(xiàn)的隐解,相對于synchronizedd在功能性上鞍帝,有所加強,主要是煞茫,公平鎖帕涌,輪詢鎖,定時鎖续徽,可中斷鎖等蚓曼,還增加了多路通知機制(Condition),可以用一個鎖來管理多個同步塊钦扭。另外在使用的時候纫版,必須手動的釋放鎖。
詳細(xì)分析:
- Lock鎖的實現(xiàn)客情,主要是借助于隊列同步器(我們常常見到的AQS)來實現(xiàn)其弊。它包括一個int變量來表示狀態(tài)会涎;一個FIFO隊列,來存儲獲取資源的排隊線程瑞凑。
- 當(dāng)一個線程申請資源時,就是是獲取當(dāng)前的同步狀態(tài)概页,并判斷是否可符合預(yù)期籽御,如果是,則通過CAS操作惰匙,來修改上述Int變量標(biāo)識的同步狀態(tài)技掏。如果否,則線程進(jìn)入隊列排隊(這是在一般情況项鬼,在使用tyrLock時哑梳,是直接返回獲取鎖失敗)绘盟。
鎖有獨占鎖和共享鎖鸠真。獨占鎖就是在同一時刻,只允許同一個線程持有該鎖龄毡;共享鎖實現(xiàn)的時候和獨占鎖稍有不同吠卷,不是簡單的修改同步狀態(tài)(比如1和0),而是獲取這個值沦零,當(dāng)值大于0時祭隔,即標(biāo)識獲取共享鎖成功(隱含意思是每個線程獲取鎖成功后,這個值減1)路操。這里附上獨占鎖的實現(xiàn)源碼(源碼片段來自《java并發(fā)編程的藝術(shù)》疾渴,并加上自己的注釋):
public class Mutex implements Lock {
// 靜態(tài)內(nèi)部類,自定義同步器
private static class Sync extends AbstractQueuedSynchronizer{
// 該方法用于判斷當(dāng)前鎖是否在獨占模式下被占用狀態(tài)
protected boolean isHeldExclusively(){
return getState() == 1;
}
// 獲取鎖!!!
public boolean tryAcquire(int acquires){
//典型的CAS原子操作屯仗,如果初始狀態(tài)為0搞坝,可以獲得鎖
if (compareAndSetState(0, 1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//釋放鎖,將當(dāng)前狀態(tài)設(shè)置為0
protected boolean tryRelease(int releases){
if (getState() == 0){
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一個Condition祭钉,每個condition都包含了一個condition隊列 瞄沙,這個后續(xù)再說
Condition newCondition(){
return new ConditionObject();
}
}
- Lock鎖中,支持可中斷的鎖慌核,實現(xiàn)原理是距境,隊列中的等待線程,可以響應(yīng)其他線程發(fā)起的中斷信號垮卓,拋出InterruptdException異常垫桂。
- 關(guān)于同步隊列,需要了解粟按,獲取同步狀態(tài)失敗的線程诬滩,被包裝為Node節(jié)點后霹粥,加入隊列尾,這個操作是CAS操作疼鸟,以保證線程安全后控,失敗就死循環(huán)重試;而隊列首節(jié)點空镜,則是當(dāng)前持有鎖的線程浩淘。該節(jié)點一旦釋放鎖缓窜,會喚醒后繼節(jié)點币旧。
- 關(guān)于喚醒灾炭,是這樣的檬嘀,每個在同步隊列中的阻塞線程峭状,都處于自旋的狀態(tài)谅河,不斷的嘗試獲取鎖行施。這樣延刘,當(dāng)首節(jié)點釋放鎖喚醒后繼線程后镣隶,被喚醒的線程极谊,還需要判斷是否前繼線程是首線程,是則獲取同步狀態(tài)(鎖)成功矾缓。
擴展:Condition怀酷,多路通知機制
- 在Synchronized鎖中,提供了wait嗜闻、notify蜕依、notifyAll等方法,實現(xiàn)了等待/通知模式琉雳。那么在lock中样眠,由Condition配合,也實現(xiàn)了類似的模式翠肘。
- 其實現(xiàn)實質(zhì)是檐束,一個Condition包含一個等待隊列,定義多個Condition束倍,那就有多個等待隊列被丧,和上文提到的同步隊列配合使用。同步隊列-等待隊列模型請參
- 在上述模型中绪妹,調(diào)用await方法甥桂,相當(dāng)于把同步隊列首節(jié)點(持有鎖的線程),移動到等待隊列邮旷。調(diào)用signal方法喚醒阻塞的線程黄选,則是將對應(yīng)Condition等待隊列里的首節(jié)點(等待時間最長),移入同步隊列婶肩。
- 還有一點需要補充办陷,就是線程的喚醒貌夕,調(diào)用signal可以正常喚醒;在其他線程中終止線程民镜,也一樣會喚醒啡专,只不過喚醒后,只是拋出InterruptException異常制圈。