2019.12-2020.02后端面試材料分享拓春,架構(gòu)/中間件篇。
拿到了字節(jié)offer亚隅,走完了Hello單車和達(dá)達(dá)的面試流程(沒給offer)硼莽,螞蟻的前三輪(接了字節(jié)Offer,放棄后續(xù)流程)煮纵。
以下問題匯總在一個類anki的小程序:一進(jìn)制懂鸵。默認(rèn)隱藏答案,思考后再點開對照行疏;根據(jù)你反饋的難度匆光,安排復(fù)習(xí)時間。
-問題1: 邊用iterator遍歷hashMap终息,邊通過hashMap自身方法修改數(shù)據(jù),有什么問題货葬?
-tags: java,hashmap,并發(fā)
-解答:
會拋出ConcurrentModificationException
采幌。
因為HashMap的modCount和Iterator維護(hù)的expectedModCount不相同了。正確的做法是只通過HashMap
本身或者只通過Iterator
去修改數(shù)據(jù)震桶。
-問題2: 相比Java7休傍,Java8的ConcurrentHashMap做了什么改進(jìn)?
-tags: java,hashmap,并發(fā)
-解答:
-
數(shù)據(jù)結(jié)構(gòu)蹲姐。Java7為實現(xiàn)并行訪問磨取,引入了
Segment
這一結(jié)構(gòu),實現(xiàn)了分段鎖柴墩,理論上最大并發(fā)度與Segment
個數(shù)相等忙厌。Java8為進(jìn)一步提高并發(fā)性,摒棄了分段鎖的方案江咳,而是直接使用一個大的數(shù)組逢净。同時,Java 8在鏈表長度超過一定閾值(8)時將鏈表(尋址時間復(fù)雜度為O(N))轉(zhuǎn)換為紅黑樹(尋址時間復(fù)雜度為O(long(N)))歼指。-
同步方式爹土。如果數(shù)據(jù)未初始化,或者是更新桶的第一個元素踩身,則通過
CAS
操作胀茵,無需加鎖。否則通過synchronized
獲取桶(鏈表或紅黑樹第一個節(jié)點)的鎖挟阻,相對分段鎖呵恢,鎖的顆粒度更細(xì)了。實際上好渠,生產(chǎn)環(huán)境中,map
在放入時競爭同一個鎖的概率非常小瞒瘸,分段鎖反而會造成更新等操作的長時間等待,且java8的內(nèi)置鎖比之前版本優(yōu)化了很多挪拟,相較ReentrantLock
挨务,性能不并差击你。數(shù)組本身用volatile
修飾:transient volatile Node<K,V>[] table;
玉组,數(shù)組元素由Unsafe
的getObjectVolatile
保證可見性,元素內(nèi)的字段要么是final
要么是volatile
修飾丁侄,可見性也有保障惯雳。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; }
-
size方法。在Java7中鸿摇,會使用不加鎖的模式去嘗試多次計算
ConcurrentHashMap
的size石景,最多三次,比較前后兩次計算的結(jié)果拙吉,結(jié)果一致就認(rèn)為當(dāng)前沒有元素加入潮孽,計算的結(jié)果是準(zhǔn)確的。 如果計算不一致筷黔,就會給每個 Segment 加上鎖往史,然后計算ConcurrentHashMap
的size
返回。
-
同步方式爹土。如果數(shù)據(jù)未初始化,或者是更新桶的第一個元素踩身,則通過
Java8中佛舱,put
方法和remove
方法都會通過addCount
方法維護(hù)Map的size椎例,size方法通過sumCount獲取由addCount方法維護(hù)的Map的size。
addCount
方法統(tǒng)計數(shù)值baseCount
(正常無并發(fā)下的節(jié)點數(shù)量)和counterCells
(并發(fā)插入下的節(jié)點數(shù)量)请祖,以精確計算并發(fā)讀寫情況下table中元素的數(shù)量订歪。它首先嘗試用CAS更新baseCount
,成功肆捕,如果CAS操作失敗刷晋,則表示有競爭,有其他線程并發(fā)插入慎陵,則修改的數(shù)量會被記錄到CounterCell
中眼虱。
-問題3: 靜態(tài)數(shù)據(jù)、構(gòu)造函數(shù)荆姆、代碼段蒙幻、字段,執(zhí)行的順序是怎樣的胆筒?
當(dāng)創(chuàng)建類的實例時邮破,父類和子類的靜態(tài)數(shù)據(jù)诈豌、構(gòu)造函數(shù)、代碼段抒和、字段矫渔,執(zhí)行的順序是怎樣的?
-tags: java
-解答:
按照先父類后子類摧莽、先靜態(tài)后動態(tài)的順序:
- 父類靜態(tài)變量
- 父類靜態(tài)代碼塊
- 子類靜態(tài)變量
- 子類靜態(tài)代碼塊
- 父類非靜態(tài)變量(父類實例成員變量)
- 父類構(gòu)造函數(shù)
- 子類非靜態(tài)變量(子類實例成員變量)
- 子類構(gòu)造函數(shù)
-問題4: 有沒有有順序的 Map 實現(xiàn)類庙洼, 如果有, 它們是怎么保證有序的?
-tags: java
-解答:
-
TreeMap
镊辕,默認(rèn)按key升序油够,可按創(chuàng)建時給定的Comparator
排序。 - LinkedHashMap征懈,雙向鏈表石咬,記錄了插入順序;也支持訪問順序卖哎,內(nèi)部維護(hù)LRU使得key從Least Recently Accessed到Most Recently Accessed排序鬼悠。
-問題5: error和exception的區(qū)別,CheckedException,RuntimeException的區(qū)別
-tags: java
-解答:
- error,程序無法處理的錯誤亏娜,發(fā)生于虛擬機(jī)自身焕窝,可能會導(dǎo)致線程中斷。
- exception维贺,操作或操作可能會引發(fā)的錯誤,可以被程序處理
- CheckedException它掂,檢查型異常,強(qiáng)制要求必須做異常處理幸缕。
- RuntimeException群发,運行時異常,因操作引發(fā)的異常发乔,“程序雖然無法繼續(xù)執(zhí)行熟妓,但是還能搶救一下”,不要求強(qiáng)制做異常處理栏尚。
額外補(bǔ)充:
- 異常實例的構(gòu)造十分昂貴是由于在構(gòu)造異常實例時起愈,Java 虛擬機(jī)便需要生成該異常的棧軌跡(stack trace)。要逐一訪問當(dāng)前線程的 Java 棧幀译仗,并且記錄下調(diào)試信息抬虽,包括棧幀所指向方法的名字,方法所在的類名纵菌、文件名和異常行號等阐污。
- finally能實現(xiàn)無論異常與否都能被執(zhí)行的,是由于編譯器在編譯Java代碼時咱圆,會復(fù)制finally代碼塊的內(nèi)容笛辟,然后分別放在try-catch代碼塊所有的正常執(zhí)行路徑及異常執(zhí)行路徑的出口中功氨。
- 如果finally有return語句,catch內(nèi)throw的異常會被忽略手幢。因為catch里拋的異常會被finally捕獲捷凄,在執(zhí)行完finally代碼后重新拋出該異常。由于finally代碼塊有個return語句围来,在重新拋出前就返回了跺涤。
-問題6: 自己創(chuàng)建一個java.lang.String對象,會被類加載器加載嗎监透?
在自己的代碼中桶错,創(chuàng)建一個java.lang.String
對象,會被類加載器加載嗎才漆,為什么
-tags: java
-解答:
不可以
啟動類加載器(bootstrap)會將Java_Home/lib下面的類庫加載到內(nèi)存中,所以自己創(chuàng)建的java.lang.String對象無法被加載
-問題7: 分代GC中牛曹,對象從創(chuàng)建到回收的一般流程是怎樣的佛点?
-tags: java
-解答:
HotSpot JVM把年輕代分為了三部分: 1個Eden區(qū)和2個Survivor區(qū)(分別叫from和to)醇滥,默認(rèn)大小比例為8:1。
一般情況下超营,新創(chuàng)建的對象都會被分配到Eden區(qū)(一些大對象特殊處理),這些對象經(jīng)過第一次Minor GC后鸳玩,如果仍然存活,將會被移到Survivor區(qū)演闭。對象在Survivor區(qū)中每熬過一次Minor GC不跟,年齡就會增加1歲,當(dāng)它的年齡增加到一定程度時米碰,就會被移動到年老代中窝革。
因為年輕代中的對象基本都是朝生夕死的(80%以上),所以在年輕代的垃圾回收算法使用的是復(fù)制算法吕座,在GC開始的時候虐译,對象只會存在于Eden區(qū)和名為“From”的Survivor區(qū),Survivor區(qū)“To”是空的吴趴。緊接著進(jìn)行GC漆诽,Eden區(qū)中所有存活的對象都會被復(fù)制到“To”。
Full GC 是發(fā)生在老年代的垃圾收集動作锣枝,采用的是標(biāo)記-清除算法厢拭。該算法會產(chǎn)生內(nèi)存碎片,此后為較大的對象分配內(nèi)存空間時撇叁,若無法找到足夠的連續(xù)的內(nèi)存空間供鸠,就會提前觸發(fā)一次 GC 的收集動作。
-問題8: GC的觸發(fā)條件有哪些陨闹?
-tags: java
-解答:
GC 觸發(fā)條件
young GC:當(dāng)young gen中的eden區(qū)分配滿的時候觸發(fā)楞捂。(有部分存活對象會晉升到old gen家制,所以young GC后old gen的占用量通常會有所升高)。
-
full GC:
當(dāng)準(zhǔn)備要觸發(fā)一次young GC時泡一,如果發(fā)現(xiàn)之前young GC的平均晉升大小比目前old gen剩余的空間大颤殴,則不會觸發(fā)young GC而是轉(zhuǎn)為觸發(fā)full GC(包括young gen,所以不需要事先觸發(fā)一次單獨的young GC)鼻忠;
如果有perm gen的話涵但,要在perm gen分配空間但已經(jīng)沒有足夠空間時,也要觸發(fā)一次full GC帖蔓;或者System.gc()矮瘟、heap dump帶GC,默認(rèn)也是觸發(fā)full GC塑娇。
-問題9: 談?wù)剬pring框架設(shè)計理念的理解
Spring解決了什么關(guān)鍵的問題澈侠?它有哪幾個核心組件?為什么需要這些組件埋酬?它們是如何結(jié)合在一起構(gòu)成Spring的骨骼架構(gòu)的哨啃?
-tags: java,spring
-解答:
Spring框架中的核心組件只有三個:Context ,Core和Beans写妥,它們構(gòu)建起了整個Spring 的骨骼架構(gòu)拳球。其中Beans又是核心中的核心。
關(guān)鍵問題
Spring如此流行珍特,是因為解決了一個非常關(guān)鍵的問題:它把對象間的依賴關(guān)系轉(zhuǎn)而用配置文件來管理祝峻,也就是它的依賴注入機(jī)制。而這個注入關(guān)系在一個叫IOC 的容器中管理扎筒。Spring 正是通過把對象包裝在Bean中來達(dá)到對這些對象管理以及額外操作的目的莱找。
協(xié)同工作
我們把Bean比作一場演出中的演員,那么Context就是舞臺背景嗜桌,Core就是演出的道具奥溺。
對Context 來說它就是要發(fā)現(xiàn)每個Bean之間的關(guān)系,為它們建立這種關(guān)系并維護(hù)好這種關(guān)系症脂。所以Context就是一個 Bean關(guān)系的集合谚赎,這個關(guān)系集合又叫IOC容器。一旦建立起這個IOC容器之后诱篷, Spring就可以為你工作了壶唤。那么Core組件又有何用武之地呢?其實Core 就是發(fā)現(xiàn)棕所,建立和維護(hù)Bean之間的關(guān)系所需要的一系列工具闸盔,從這個角度看Core這個組件叫Util更能讓你理解。
IOC容器如何工作
IOC容器實際上就是 Context組件結(jié)合其他兩個組件共同構(gòu)建了一個Bean的關(guān)系網(wǎng)琳省。網(wǎng)的構(gòu)建的入口就在AbstractApplicationContext類的refresh方法中迎吵,它做了以下事情:
- 構(gòu)建BeanFactory躲撰,以便產(chǎn)生所需的“演員”
- 注冊可能感興趣的事件
- 創(chuàng)建Bean實例對象
- 觸發(fā)被監(jiān)聽的時間
-問題10: 準(zhǔn)備用HashMap存1w條數(shù)據(jù),構(gòu)造時傳10000還會觸發(fā)擴(kuò)容嗎击费?
準(zhǔn)備用HashMap存1w條數(shù)據(jù)拢蛋,構(gòu)造時傳10000還會觸發(fā)擴(kuò)容嗎?
如果是準(zhǔn)備存1k條蔫巩,構(gòu)造時傳1000呢谆棱?
-tags: java
-解答:
不會。
以JDK8
的源碼來說明:
- HashMap是否擴(kuò)容圆仔,由threshold決定垃瞧,而threshold又由初始容量和 loadFactor決定。
- 構(gòu)造方法傳遞的 initialCapacity坪郭,最終會被 tableSizeFor() 方法動態(tài)調(diào)整為2 的 N 次冪个从,以方便在擴(kuò)容的時候,計算數(shù)據(jù)在 newTable 中的位置歪沃。
- 如果設(shè)置了 table 的初始容量嗦锐,會在初始化 table 時,將擴(kuò)容閾值 threshold 重新調(diào)整為 table.size * loadFactor绸罗。
那么
- 當(dāng)我們從外部傳遞進(jìn)來 1w 時意推,實際上經(jīng)過 tableSizeFor() 方法處理之后,就會變成 2 的 14 次冪 16384珊蟀,再算上負(fù)載因子 0.75f,實際在不觸發(fā)擴(kuò)容的前提下外驱,可存儲的數(shù)據(jù)容量是 12288(16384 * 0.75f)育灸,已經(jīng)大于10000了。
- 當(dāng)HashMap 初始容量指定為1000時昵宇,會被調(diào)整為 1024磅崭,但是它只是表示 table 數(shù)組大小,擴(kuò)容的重要依據(jù)擴(kuò)容閾值會在 resize() 中調(diào)整為 768(1024 * 0.75)瓦哎。
它是不足以承載 1000 條數(shù)據(jù)的砸喻。
-問題11: Cpu飆高,jstack發(fā)現(xiàn)最耗cpu的線程卻是waiting狀態(tài)
通過top -Hp $pid 找到最耗CPU的線程蒋譬,再jstack, 到輸出里查這個線程割岛,發(fā)現(xiàn)它卻被LockSupport.park掛起了,處于WAITING狀態(tài)犯助。這是怎么回事癣漆?
-tags: java,并發(fā)
-解答:
首先,LockSupport.park的注釋里明確提到park的線程不會再被CPU調(diào)度的剂买。所以可以大膽推斷不是線程本身的代碼消耗cpu惠爽。
那么癌蓖,最有可能的便是線程的上下文切換。如果不確信它可以占用多少資源婚肆,可以做個實驗租副,啟動幾千個線程,用LockSupport.park()不斷掛起這些線程较性, 再使用LockSupport.unpark(t)不斷地喚醒這些線程.附井,喚醒之后又立馬掛起,觀察期間cpu的使用情況便可知道两残。
那么永毅,問題是,具體是什么消耗了cpu呢人弓?從代碼調(diào)用棧中找答案沼死。
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
AQS是很多jdk并發(fā)庫的底層框架,
- AQS有個臨界變量state崔赌,當(dāng)一個線程獲取到state==0時, 表示這個線程進(jìn)入了臨界代碼(獲取到鎖)意蛀, 并原子地把這個變量值+1
- 沒能進(jìn)入臨界區(qū)(獲取鎖失敗)的線程,會利用CAS操作添加到到CLH隊列尾去, 并被LockSupport.park掛起
- 當(dāng)線程釋放鎖的時候, 會喚醒head節(jié)點的下一個需要喚醒的線程
- 被喚醒的線程檢查一下自己的前置節(jié)點是不是head節(jié)點(CLH隊列的head節(jié)點就是之前拿到鎖的線程節(jié)點)的下一個節(jié)點健芭,如果不是則繼續(xù)掛起, 如果是的話, 與其他線程重新爭奪臨界變量,即重復(fù)第1步
在AQS的第2步中, 如果競爭鎖失敗的話, 是會使用CAS樂觀鎖的方式添加到隊列尾的, 核心代碼如下
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在并發(fā)量非常高的情況下, 每一次執(zhí)行compareAndSetTail都失敗(即返回false)的話县钥,那么這段代碼就相當(dāng)是一個死循環(huán),消耗cpu就不奇怪了慈迈。
更進(jìn)一步若贮,如果進(jìn)入臨界區(qū)后很快就做完了業(yè)務(wù)邏輯,會導(dǎo)致CLH隊列的線程被頻繁喚醒痒留,而又由于搶占鎖失敗頻繁地被掛起谴麦,也會帶來大量的上下文切換, 消耗系統(tǒng)的cpu資源。
-問題12: 詭異的NaN
浮點數(shù)NaN(Not-a-Number)有什么“詭異”的特性伸头?
-tags: java
-解答:
先繞圈講點別的匾效。
在 Java 中,正無窮和負(fù)無窮是有確切的值恤磷,在內(nèi)存中分別等同于十六進(jìn)制整數(shù) 0x7F800000 和 0xFF800000面哼。
這個范圍之外,即:[0x7F800001, 0x7FFFFFFF] 和 [0xFF800001, 0xFFFFFFFF] 對應(yīng)的就是NaN
扫步。
NaN 有一個有趣的特性:除了“!=”始終返回 true 之外魔策,所有其他比較結(jié)果都會返回 false。舉例來說锌妻,“NaN<1.0F”返回 false代乃,而“NaN>=1.0F”同樣返回 false。對于任意浮點數(shù) f,不管它是 0 還是 NaN搁吓,“f!=NaN”始終會返回 true原茅,而“f==NaN”始終會返回 false。
-問題13: java中byte和long類型變量分別占用多少內(nèi)存空間
-tags: java
-解答:
boolean堕仔、byte擂橘、char、short 這四種類型摩骨,在棧上占用的空間和 int 是一樣的通贞。因此,在 32 位的 HotSpot 中恼五,這些類型在棧上將占用4個字節(jié)昌罩;而在 64 位的 HotSpot 中,他們將占8個字節(jié)灾馒。
當(dāng)然茎用,對于 byte、char 以及 short 這三種類型睬罗,它們在堆上占用的空間分別為一字節(jié)轨功、兩字節(jié),以及兩字節(jié)容达,也就是說古涧,跟這些類型的值域相吻合。
無論在哪里花盐,long
和double
類型都占用8字節(jié)羡滑。
-問題14: jvm加載類的過程
-tags: java
-解答:
jvm加載java類就是將字節(jié)流文件加入到內(nèi)存中的過程,分為以下三步:加載卒暂、鏈接啄栓、初始化。
加載:查找字節(jié)流并且據(jù)此創(chuàng)建類的過程也祠,每一種類加載器加載一部分類。
- 加載規(guī)則:雙親委派機(jī)制
- 類的唯一性:類加載器名稱+類全限定名稱
- 類加載器:
- 啟動類加載器:無對應(yīng)的java對象近速,負(fù)責(zé)加載最基礎(chǔ)的類诈嘿。如jre/lib下的類。
- 擴(kuò)展類加載器:有對應(yīng)的java對象削葱,父類啟動類加載器奖亚,負(fù)責(zé)加載jre/ext下類,該類加載器被啟動類加載器加載之后方能加載其他類析砸。
- 應(yīng)用類加載器:有對應(yīng)的java對象昔字,父類是擴(kuò)展類加載器,負(fù)責(zé)加載應(yīng)用程序路徑下的類,classpath作郭、系統(tǒng)變量java.class.path或者環(huán)境變量classpath指定的類陨囊。
鏈接:驗證、準(zhǔn)備夹攒、解析
- 驗證:在于確定被加載類滿足jvm的約束條件蜘醋。
- 準(zhǔn)備:為被加載類的靜態(tài)字段分配內(nèi)存,構(gòu)造與該類相關(guān)聯(lián)的方法表咏尝。
- 解析:將符號引用解析為實際引用压语,符號引用是在編譯階段由編譯器生成,包含目標(biāo)方法所在類的名字编检、目標(biāo)方法的名字胎食、接收參數(shù)類型以及返回值類型。
初始化:為標(biāo)記為常量值的字段(基本類型或字符串且被修飾為final)賦值允懂,以及執(zhí)行<clinit>方法(其他賦值操作和靜態(tài)代碼塊)
- 類的初始化過程是線程安全的厕怜,并且只能被初始化一次。jvm會通過加鎖來保證<clinit>方法僅被執(zhí)行一次
- 初始化的時機(jī):對一個類的主動引用累驮。
被動引用并不會引發(fā)類的初始化酣倾,如引用類的靜態(tài)常量,引用父類的靜態(tài)字段不會初始化子類谤专,數(shù)組定義帶來的引用不會導(dǎo)致初始化躁锡。
-問題15: Java虛擬機(jī)調(diào)用方法的大致過程
方法是如何被找到和執(zhí)行的?
-tags: java
-解答:
Java 里所有非私有實例方法調(diào)用都會被編譯成 invokevirtual 指令置侍,而接口方法調(diào)用都會被編譯成 invokeinterface 指令映之。這兩種指令,均屬于 Java 虛擬機(jī)中的虛方法調(diào)用蜡坊。這里只解釋虛方法的調(diào)用 過程杠输。
如果這兩種指令所聲明的目標(biāo)方法被標(biāo)記為 final,那么 Java 虛擬機(jī)會采用靜態(tài)綁定秕衙。否則蠢甲,Java 虛擬機(jī)將采用動態(tài)綁定,在運行過程中根據(jù)調(diào)用者的動態(tài)類型据忘,來決定具體的目標(biāo)方法鹦牛。
而動態(tài)綁定又是通過方法表這一數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)的。方法表本質(zhì)上是一個數(shù)組勇吊,每個數(shù)組元素指向一個當(dāng)前類及其祖先類中非私有的實例方法曼追。
在解析虛方法調(diào)用時,Java 虛擬機(jī)會紀(jì)錄下所聲明目標(biāo)方法的索引值汉规,并且在運行過程中根據(jù)這個索引值查找具體的目標(biāo)方法礼殊。這個過程便是動態(tài)綁定。
Java 虛擬機(jī)中的即時編譯器會使用內(nèi)聯(lián)緩存來加速動態(tài)綁定。Java 虛擬機(jī)所采用的單態(tài)內(nèi)聯(lián)緩存將紀(jì)錄調(diào)用者的動態(tài)類型晶伦,以及它所對應(yīng)的目標(biāo)方法碟狞。當(dāng)碰到新的調(diào)用者時,如果其動態(tài)類型與緩存中的類型匹配坝辫,則直接調(diào)用緩存的目標(biāo)方法篷就。
-問題16: java反射慢的原因是什么
都說java反射慢,究竟慢在哪了近忙,為什么慢竭业?
-tags: java
-解答:
以使用java
反射調(diào)用方法為例,一般流程是:
先用Class.forName獲取類對象及舍,再用Class.getMethod獲取方法未辆,最后執(zhí)行Method.invoke進(jìn)行動態(tài)調(diào)用。
其中锯玛,Class.forName會調(diào)用本地方法咐柜,Class.getMethod則會遍歷該類的公有方法。如果沒有匹配到攘残,還將遍歷父類的公有方法拙友。所以這兩個操作都非常費時。
另外歼郭,getMethod返回的是結(jié)果的一份拷貝遗契。在熱點代碼中頻率使用它或類似的getMethods或getDeclaredMethods會帶來較多的堆空間消耗。
反射調(diào)用本身也有較高性能消耗病曾。
- Method.invoke是個變長參數(shù)方法牍蜂,編譯器會在調(diào)用處生成一個Object 數(shù)組,保存?zhèn)魅氲膮?shù)泰涂。如果參數(shù)是基本類型鲫竞,還得進(jìn)行自動裝箱(因為Object數(shù)組只能保存包裝類型)猪落。最重要的一點是悔捶,因為是間接調(diào)用饺律,導(dǎo)致反射調(diào)用無法被內(nèi)聯(lián)惠啄。
-問題17: invokedynamic指令有何特點?
和其它方法調(diào)用指令invokestatic成箫,invokespecial醉顽,invokevirtual等相比并齐,invokedynamic有何特點妖泄?
-tags: java
-解答:
上述指令中,Java 虛擬機(jī)明確要求方法調(diào)用需要提供目標(biāo)方法的類名艘策。它們使用上不夠靈活蹈胡,執(zhí)行效率也不高。
為此,Java 7 引入了一條新的指令 invokedynamic罚渐。該指令抽象出調(diào)用點這一個概念却汉,并允許應(yīng)用程序?qū)?strong>調(diào)用點鏈接至任意符合條件的方法上。
其底層依賴方法句柄荷并,這是一個強(qiáng)類型的合砂、能夠被直接執(zhí)行的引用。它僅關(guān)心所指向方法的參數(shù)類型以及返回類型源织,而不關(guān)心方法所在的類以及方法名翩伪。
方法句柄的權(quán)限檢查發(fā)生在創(chuàng)建過程中,相較于反射調(diào)用谈息,節(jié)省了調(diào)用時反復(fù)檢查權(quán)限的開銷缘屹。
-問題18: gc的安全點概念
- 什么是安全點?
- 哪些狀態(tài)屬于安全點侠仇?
- 會出現(xiàn)長時間達(dá)到不安全點的情況嗎轻姿?
-tags: java
-解答:
什么是安全點
安全點(safepoint)是在代碼執(zhí)行過程中的特殊位置,當(dāng)線程執(zhí)行到這些位置時逻炊,可以暫停互亮,安全地進(jìn)行GC,而不會引發(fā)混亂余素。
哪些狀態(tài)屬于安全點豹休?
對線程可能在干的事一個個討論:
- 執(zhí)行 JNI 本地代碼,如果這段代碼不訪問 Java 對象溺森、調(diào)用 Java 方法或者返回至原 Java 方法慕爬,那么 Java 虛擬機(jī)的堆棧不會發(fā)生改變。這種情況下可以作為安全點屏积。
- 解釋執(zhí)行字節(jié)碼医窿,字節(jié)碼與字節(jié)碼之間可作為安全點。
- 執(zhí)行即時編譯器生成的機(jī)器碼炊林,比較復(fù)雜姥卢,在第三問中一并解釋。
- 線程阻塞渣聚,此時線程被虛擬機(jī)線程調(diào)度器管理独榴,屬于安全點。
會出現(xiàn)長時間達(dá)到不安全點的情況嗎奕枝?
不會棺榔。從第二問的分析看,當(dāng)在虛擬機(jī)掌握范圍內(nèi)時隘道,虛擬機(jī)可以方便地進(jìn)行安全點檢測症歇。需要考慮的是執(zhí)行本地代碼或即時編譯的機(jī)器碼時郎笆。
- 事實上,由于本地代碼需要通過 JNI 的 API 來訪問java對象忘晤、調(diào)用java方法宛蚓,或返回java方法,因此虛擬機(jī)僅需在 API 的入口處進(jìn)行安全點檢測即可搞定所有本地代碼情形设塔。
- 針對機(jī)器碼凄吏,因為不受 Java 虛擬機(jī)掌控,因此需要在生成機(jī)器碼時闰蛔,插入安全點檢測痕钢,以避免機(jī)器碼長時間沒有安全點檢測的情況。
-問題19: 垃圾回收的基本方式有哪幾種钞护,分別有何優(yōu)缺點
比如其中一種是“清除”
-tags: java
-解答:
一盖喷,清除
把死亡對象所占據(jù)的內(nèi)存標(biāo)記為空閑內(nèi)存,并記錄在一個空閑列表(free list)之中难咕。當(dāng)需要新建對象時课梳,內(nèi)存管理模塊便會從該空閑列表中尋找空閑內(nèi)存,并劃分給新建的對象余佃。
缺點是
- 易造成內(nèi)存碎片暮刃。
- 分配效率較低。需要遍歷列表爆土,來查找足夠大的空閑內(nèi)存椭懊。
二,壓縮
把存活的對象聚集到內(nèi)存區(qū)域的起始位置步势,從而留下一段連續(xù)的內(nèi)存空間氧猬。
缺點是壓縮算法有較大性能開銷。
三坏瘩,復(fù)制
把內(nèi)存區(qū)域分為兩等分盅抚,總是把存活對象復(fù)制到其中一半,騰空的另一半給新對象用倔矾,如此來回倒騰妄均。缺點是堆空間的使用效率極其低下。
-問題20: 新生代和老年代分別有哪些適用的垃圾回收器哪自?
各自有什么特點丰包?
-tags: java
-解答:
新生代
- Serial,單線程
- Parallel New壤巷,多線程
- Parallel Scavenge邑彪,多線程+注重吞吐率 + 不能與CMS共用。
老年代
- Serial Old, 單線程胧华,標(biāo)記 - 壓縮算法
- Parallel Old, 多線程, 標(biāo)記 - 壓縮算法
- CMS, 能與業(yè)務(wù)并發(fā)锌蓄,標(biāo)記 - 清除算法升筏。
-問題21: 請描述G1垃圾回收器
-tags: java
-解答:
G1直接將堆分成很多個區(qū)域。每個區(qū)域都可以充當(dāng)Eden區(qū)瘸爽、Survivor區(qū)或者老年代中的一個。
它采用的是標(biāo)記 - 壓縮算法铅忿,而且和 CMS 一樣都能夠在應(yīng)用程序運行過程中并發(fā)地進(jìn)行垃圾回收剪决。
G1在選擇進(jìn)行垃圾回收的區(qū)域時,會優(yōu)先回收死亡對象較多的區(qū)域檀训。
-問題22: synchronized所加的鎖可能有幾種柑潦,輕重排序是怎樣的?
-tags: java
-解答:
按照從重到輕的排序峻凫,可能是重量級鎖渗鬼、輕量級鎖、偏向鎖荧琼。
重量級鎖狀態(tài)下譬胎,加鎖失敗的線程會被阻塞,喚醒也需要靠操作系統(tǒng)命锄,成本比較高堰乔。為了改善成本,虛擬機(jī)會在線程進(jìn)入阻塞狀態(tài)之前脐恩,以及被喚醒后競爭不到鎖的情況下镐侯,進(jìn)入自旋狀態(tài)。通俗地理解驶冒,阻塞好比熄火停車苟翻,自旋好比怠速停車。
而輕量級鎖好比深夜的十字路口骗污,四個方向都閃黃燈的情況崇猫,車很少,偶爾一兩輛身堡,自行觀察后通過即可邓尤。
偏向鎖更進(jìn)一步,好比能識別救護(hù)車的紅綠燈贴谎,如果匹配到救護(hù)車汞扎,直接亮綠燈放行。
-問題23: 輕量級鎖的加鎖和解鎖過程是怎樣的擅这?
-tags: java
-解答:
加鎖
首先在當(dāng)前線程的當(dāng)前棧楨中劃出一塊空間澈魄,作為該鎖的鎖記錄,將鎖對象的標(biāo)記字段復(fù)制到該鎖記錄中仲翎。然后痹扇,虛擬機(jī)會嘗試用CAS操作替換鎖對象的標(biāo)記字段铛漓。
解鎖
- 如果當(dāng)前鎖記錄的值為 0,則代表重復(fù)進(jìn)入同一把鎖鲫构,直接返回即可浓恶。
- 否則,Java 虛擬機(jī)會嘗試用CAS操作结笨,比較鎖對象的標(biāo)記字段的值是否為當(dāng)前鎖記錄的地址包晰。
- 如果是,則替換為鎖記錄中的值炕吸,也就是鎖對象原本的標(biāo)記字段伐憾。此時,該線程已經(jīng)成功釋放這把鎖赫模。
- 如果不是树肃,則意味著這把鎖已經(jīng)被膨脹為重量級鎖。
-問題24: 什么是即時編譯的分層編譯
-tags: java
-解答:
從Java 8開始瀑罗,JVM默認(rèn)采用分層編譯的方式胸嘴,分為:
- 0 層,解釋執(zhí)行
- 1 層廓脆,使用C1即時編譯器(對應(yīng)參數(shù)-client)筛谚,不帶profiling
- 2 層,使用C1編譯器停忿,執(zhí)行部分 profiling
- 3 層驾讲,使用C1編譯器,執(zhí)行全部 profiling
- 4 層席赂,使用C2編譯器(對應(yīng)參數(shù)-server)
通常情況下吮铭,C2 代碼的執(zhí)行效率要比 C1 代碼的高出 30% 以上。
方法會首先被解釋執(zhí)行颅停,然后被 3 層的 C1 編譯谓晌,最后被 4 層的 C2 編譯。
第3層C1編譯中癞揉,profile收集的關(guān)于分支以及類型的數(shù)據(jù)纸肉,可用來推斷程序今后的執(zhí)行。這些推斷會精簡代碼的控制流以及數(shù)據(jù)流喊熟。在假設(shè)失敗的情況下柏肪,JVM將去優(yōu)化,退回至解釋執(zhí)行并重新收集相關(guān)profile芥牌。
-問題25: Spring創(chuàng)建動態(tài)代理有哪些方式烦味,各種有何特點?
-tags: java
-解答:
有兩種方式:
-
一壁拉,使用java反射
- 利用反射機(jī)制生成一個實現(xiàn)代理接口的匿名類
- 使用InvokeHandler進(jìn)行具體方法調(diào)用
二谬俄,使用cglib柏靶,利用asm加載代理對象類的class文件,通過修改其字節(jié)碼生成子類
前者只有代理實現(xiàn)了接口的類溃论;后者不能對final修飾的類進(jìn)行代理屎蜓,也不能處理final修飾的方法。
-問題26: ThreadLocal的基本結(jié)構(gòu)是怎樣的蔬芥,什么情況下會出現(xiàn)內(nèi)存泄漏梆靖?
另外,ThreadLocalMap是如何處理hash 沖突問題的笔诵?
-tags: java,并發(fā)
-解答:
如圖,
- 每個
Thread
中都有一個ThreadLocalMap
姑子, - 每個
ThreadLocalMap
中可以有多個Entry
-
Entry
以ThreadLocal
為key
乎婿,以想要存儲的對象為value
。
再看下圖:
-
Entry
使用ThreadLocal
的弱引用作為Key
街佑,如果ThreadLocal
沒有外部強(qiáng)引用來引用它(如圖中的Thread Local Ref
為null)谢翎,那么系統(tǒng) GC 時,這個ThreadLocal
會被回收沐旨。 - 這樣一來森逮,
ThreadLocalMap
中就會出現(xiàn)key
為null
的Entry
,就沒有辦法訪問這些Entry
的value
磁携,如果這個線程遲遲不結(jié)束褒侧,就造成內(nèi)存泄漏了。
這么分析似乎是“弱引用”導(dǎo)致了泄漏谊迄,其實不是闷供。事實上,如果是強(qiáng)引用统诺,情況只會更嚴(yán)重歪脏,弱引用試圖減少泄漏的可能,只不過由于ThreadLocalMap
的生命周期跟Thread
一樣長粮呢,如果沒有手動刪除對應(yīng)key就會導(dǎo)致內(nèi)存泄漏婿失。
其實,對于key
為null
的Entry
啄寡,下一次ThreadLocalMap
調(diào)用set
豪硅、get
、remove
的時候會被清除这难。所以真正需要做的是在不需要再用到該ThreadLocal
時調(diào)用remove
清除之舟误。
Hash沖突問題
HashMap 的數(shù)據(jù)結(jié)構(gòu)是數(shù)組+鏈表,而
ThreadLocalMap的數(shù)據(jù)結(jié)構(gòu)僅僅是數(shù)組姻乓。ThreadLocalMap是通過開放地址法來解決hash沖突的問題嵌溢。
開放地址法的基本思想是一旦發(fā)生了沖突眯牧,就去尋找下一個空的散列地址,只要散列表足夠大赖草,空的散列地址總能找到学少,并將記錄存入。
比如說秧骑,我們的關(guān)鍵字集合為{12,33,4,5,15,25},表長為10版确。 我們用散列函數(shù)f(key) = key mod l0。 當(dāng)計算前S個數(shù){12,33,4,5}時乎折,都是沒有沖突的散列地址绒疗,直接存入。
計算key = 15時骂澄,發(fā)現(xiàn)f(15) = 5吓蘑,此時就與5所在的位置沖突。于是我們應(yīng)用上面的公式f(15) = (f(15)+1) mod 10 =6坟冲。于是將15存入下標(biāo)為6的位置磨镶。
這種做法有明顯的缺點:
- 容易產(chǎn)生堆積問題,不適于大規(guī)模的數(shù)據(jù)存儲健提。
- 散列函數(shù)的設(shè)計對沖突會有很大的影響琳猫,插入時可能會出現(xiàn)多次沖突的現(xiàn)象。
- 刪除的元素是多個沖突元素中的一個私痹,需要對后面的元素作處理脐嫂,實現(xiàn)較復(fù)雜。
之所以被 ThreadLocal采用侄榴,是因為:
ThreadLocal 往往存放的數(shù)據(jù)量不會特別大(而且key 是弱引用又會被垃圾回收雹锣,及時讓數(shù)據(jù)量更小)癞蚕,這個時候開放地址法簡單的結(jié)構(gòu)會顯得更省空間蕊爵,同時數(shù)組的查詢效率也是非常高,加上內(nèi)部Hash算法的設(shè)計能實現(xiàn)低沖突概率桦山。
-問題27: wait/notify機(jī)制與park/unpark機(jī)制有何異同攒射?
我們知道,Object對象的wait和notify方法可以實現(xiàn)線程的阻塞和喚醒恒水。
LockSupport的park和unpark也可以實現(xiàn)会放,那么,它們有何相同點和不同點呢钉凌?
-tags: java,并發(fā)
-解答:
先看它們的使用姿勢:
// wait/notify
synchronized (obj) {
while (<condition does not hold>)
……
obj.wait();
……
obj.notifyAll();
}
// LockSupport
LockSupport.unpark(Thread.currentThread());
LockSupport.park(Thread.currentThread());
不同點
從使用上都可以發(fā)現(xiàn)第一個不同點:
- 控制的對象不同
wait/notify的控制對線程本身來說是被動的咧最,要準(zhǔn)確的控制哪個線程、什么時候阻塞/喚醒很困難, 要不隨機(jī)喚醒一個線程(notify)要不喚醒所有的(notifyAll)
LockSupport以“線程”作為方法的參數(shù)矢沿, 語義更清晰滥搭,使用起來也更方便
- 機(jī)制不同。Object的wait/notify需要獲取對象的監(jiān)視器捣鲸,LockSupport的park/unpark不需要瑟匆。
- 調(diào)用wait/notify前必須確保獲取了對象的鎖,沒獲取鎖就調(diào)用會拋異常栽惶;而LockSupport機(jī)制是每次unpark給線程1個“許可”——最多只能是1愁溜,而park則相反,如果當(dāng)前線程有許可外厂,那么park方法會消耗許可并返回冕象,否則會阻塞線程直到線程重新獲得許可,所以你甚至可以連續(xù)調(diào)用
unpark
而不用擔(dān)心出錯:
- 調(diào)用wait/notify前必須確保獲取了對象的鎖,沒獲取鎖就調(diào)用會拋異常栽惶;而LockSupport機(jī)制是每次unpark給線程1個“許可”——最多只能是1愁溜,而park則相反,如果當(dāng)前線程有許可外厂,那么park方法會消耗許可并返回冕象,否則會阻塞線程直到線程重新獲得許可,所以你甚至可以連續(xù)調(diào)用
// 1次unpark給線程1個許可
LockSupport.unpark(Thread.currentThread());
// 如果線程非阻塞重復(fù)調(diào)用沒有任何效果
LockSupport.unpark(Thread.currentThread());
// 消耗許可
LockSupport.park(Thread.currentThread());
// 阻塞
LockSupport.park(Thread.currentThread());
開發(fā)可以不用擔(dān)心park的時序問題汁蝶,否則交惯,如果park必須要在unpark之前,那么給編程帶來很大的麻煩穿仪!wait/notify機(jī)制則比較麻煩。比如線程B要用notify通知線程A意荤,那么線程B要確保線程A已經(jīng)在wait調(diào)用上等待了啊片,否則線程A可能永遠(yuǎn)都在等待。
相同點
LockSupport的park和Object的wait一樣也能響應(yīng)中斷玖像。
-問題28: G1垃圾回收器的MaxGCPauseMillis參數(shù)
- 這是允許的GC最大的暫停時間紫谷。G1是如何做到盡量不超過這個時間的呢?
- 這個值設(shè)置得過高/過低捐寥,對業(yè)務(wù)有何影響 笤昨?
-tags: java
-解答:
第一問
先解釋一個概念,CSet(collection set):在一次垃圾收集過程中被收集的區(qū)域集合握恳。
- Young GC時:選定所有新生代里的region瞒窒。通過控制新生代的region個數(shù)來控制young GC的開銷。
- Mixed GC時:選定所有新生代里的region乡洼,外加根據(jù)全局并發(fā)標(biāo)記統(tǒng)計得出收集收益高的幾個老年代region崇裁。在用戶指定的開銷目標(biāo)范圍內(nèi)盡可能選擇收益高的老年代region。
第二問
先明確能容忍的最大暫停時間束昵,我們需要在這個限度范圍內(nèi)設(shè)置拔稳。
注意需要在吞吐量跟MaxGCPauseMillis之間做一個平衡。
如果MaxGCPauseMillis設(shè)置的過小锹雏,那么GC就會頻繁巴比,吞吐量就會下降。如果MaxGCPauseMillis設(shè)置的過大,應(yīng)用程序暫停時間就會變長轻绞。
-問題29: ThreadPoolExecutor的使用
- 有哪些搭配用的任務(wù)隊列采记?
- 何時啟用新的線程?
-tags: java,并發(fā)
-解答:
這是兩個相關(guān)的問題铲球,根據(jù)所選任務(wù)隊列的類型挺庞,ThreadPoolExecutor 會決定何時啟動一個新線程。
Direct handoffs
此時ThreadPoolExecutor 搭配的是 SynchronousQueue稼病。如果所有的線程都在忙碌选侨,而且池中的線程數(shù)尚未達(dá)到最大,則新任務(wù)會啟動一個新線程然走。這個隊列沒辦法保存等待的任務(wù):如果來了一個任務(wù)援制,創(chuàng)建的線程數(shù)已經(jīng)達(dá)到最大值,而且所有線程都在忙碌芍瑞,則新的任務(wù)總是會被拒絕晨仑。所以,建議將最大線程數(shù)指定為一個非常大的值拆檬。
Bounded queues
使用了有界隊列(如 ArrayBlockingQueue)的ThreadPoolExecutor 會采用一個非常復(fù)雜的算法洪己。比如,假設(shè)池的核心大小為 4竟贯,最大為 8答捕,所用的 ArrayBlockingQueue 最大為 10。隨著任務(wù)到達(dá)并被放到隊列中屑那,線程池中最多會運行 4 個線程(也就是核心大泄案洹)。即使隊列完全填滿持际,也就是說有 10 個處于等待狀態(tài)的任務(wù)沃琅,ThreadPoolExecutor 也是只利用 4 個線程。
如果隊列已滿蜘欲,而又有新任務(wù)加進(jìn)來益眉,此時才會啟動一個新線程。這里不會因為隊列已滿而拒絕該任務(wù)芒填,相反呜叫,會啟動一個新線程。新線程會運行隊列中的第一個任務(wù)殿衰,為新來的任務(wù)騰出空間朱庆。
Unbounded queues
如果 ThreadPoolExecutor 搭配的是無界隊列(比如 LinkedBlockedingQueue),則不會拒絕任何任務(wù)(因為隊列大小沒有限制)闷祥。這種情況下娱颊,ThreadPoolExecutor 最多僅會按最小線程數(shù)創(chuàng)建線程傲诵,也就是說,最大線程池大小被忽略了箱硕。
-問題30: ForkJoinPool的使用
- 什么是ForkJoinPool拴竹?
- 有何優(yōu)缺點?
-tags: java,并發(fā)
-解答:
首先剧罩,F(xiàn)orkJoinPool實現(xiàn)了 ExecutorService 接口栓拜,是一個標(biāo)準(zhǔn)的線程池。獨特之處在于惠昔,它是為配合分治算法的使用而設(shè)計的:任務(wù)可以遞歸地分解為子集幕与。這些子集可以并行處理,然后每個子集的結(jié)果被歸并到一個結(jié)果中镇防。
實現(xiàn)分治算法時啦鸣,會創(chuàng)建大量的任務(wù),但希望這些任務(wù)只有相對較少的幾個線程來管理来氧。
一個問題是诫给,所有任務(wù)都要等待它們派生出的任務(wù)先完成,然后才能完成啦扬。
這使得很難使用 ThreadPoolExecutor 高效實現(xiàn)這個算法中狂。ThreadPoolExecutor 內(nèi)的線程無法將另一個任務(wù)添加到隊列中并等待其完成,一旦線程進(jìn)入等待狀態(tài)扑毡,就無法使用該線程執(zhí)行它的某個子任務(wù)了吃型。
ForkJoinPool 則允許其中的線程創(chuàng)建新任務(wù),之后掛起當(dāng)前的任務(wù)僚楞。當(dāng)任務(wù)被掛起時,線程可以執(zhí)行其他等待的任務(wù)枉层。
看以下代碼片段泉褐,fork() 和 join() 方法是這里的關(guān)鍵:沒有這些方法,實現(xiàn)這類遞歸會非常痛苦鸟蜡。這些方法使用了一系列內(nèi)部的膜赃、從屬于每個線程的隊列來操縱任務(wù),并將線程從執(zhí)行一個任務(wù)切換到執(zhí)行另一個揉忘。細(xì)節(jié)對開發(fā)者是透明的跳座。
ForkJoinTask left = new ForkJoinTask(left, mid);
left.fork();
ForkJoinTask right = new ForkJoinTask(mid, right);
right.fork();
Long count = left.join() + right.join();
ForkJoinPool 還有一個額外的特性,它實現(xiàn)了工作竊绕(work-stealing)疲眷。每個工作線程都有自己所創(chuàng)建任務(wù)的隊列。線程會優(yōu)先處理自己隊列中的任務(wù)您朽,但如果這個隊列已空狂丝,它會從其他線程的隊列中竊取任務(wù)(這是個雙端隊列,從自己的隊列中取時遵循LIFO,竊取任務(wù)時遵循FIFO)几颜。其結(jié)果是倍试,即使 200 萬個任務(wù)中有一個需要很長的執(zhí)行時間,F(xiàn)orkJoinPool 中的其他線程也可以分擔(dān)其余的隨便什么任務(wù)蛋哭。ThreadPoolExecutor 則不會這樣:如果一個任務(wù)需要很長的時間县习,其他線程并不能處理額外的工作。
一般而言谆趾,如果任務(wù)是均衡的躁愿,使用分段的ThreadPoolExecutor性能更好;而如果任務(wù)是不均衡的棺妓,則使用 ForkJoinPool性能更好攘已。
-問題31: JVM是怎么實現(xiàn)synchronized的
即,用synchronized關(guān)鍵字來對程序進(jìn)行加鎖的原理
-tags: java,并發(fā)
-解答:
當(dāng)聲明 synchronized 代碼塊時怜跑,編譯而成的字節(jié)碼將包含monitorenter和 monitorexit指令样勃。這兩種指令均會使用synchronized關(guān)鍵字括號里的引用,作為所要加鎖解鎖的鎖對象性芬。
可以抽象地理解為每個鎖對象擁有一個鎖計數(shù)器和一個指向持有該鎖的線程的指針峡眶。
當(dāng)執(zhí)行 monitorenter 時,如果目標(biāo)鎖對象的計數(shù)器為 0植锉,那么說明它沒有被其他線程所持有岖瑰。在這個情況下,Java 虛擬機(jī)會將該鎖對象的持有線程設(shè)置為當(dāng)前線程婆跑,并且將其計數(shù)器加 1甲葬。
在目標(biāo)鎖對象的計數(shù)器不為 0 的情況下,如果鎖對象的持有線程是當(dāng)前線程辉饱,那么 Java 虛擬機(jī)可以將其計數(shù)器加 1搬男,否則需要等待,直至持有線程釋放該鎖彭沼。
當(dāng)執(zhí)行 monitorexit 時缔逛,Java 虛擬機(jī)則需將鎖對象的計數(shù)器減 1。當(dāng)計數(shù)器減為 0 時姓惑,那便代表該鎖已經(jīng)被釋放掉了褐奴。
當(dāng)進(jìn)行加鎖操作時,JVM還會判斷鎖的類型于毙。
對象頭中的標(biāo)記字段(mark word)的最后兩位便被用來表示該對象的鎖狀態(tài)敦冬。其中,00 代表輕量級鎖唯沮,01 代表無鎖(或偏向鎖)匪补,10 代表重量級鎖伞辛。
重量級鎖會阻塞、喚醒請求加鎖的線程夯缺。它針對的是多個線程同時競爭同一把鎖的情況蚤氏。Java 虛擬機(jī)采取了自適應(yīng)自旋,來避免線程在面對非常小的 synchronized 代碼塊時踊兜,仍會被阻塞竿滨、喚醒的情況。
輕量級鎖采用CAS操作捏境,將鎖對象的標(biāo)記字段替換為一個指針于游,指向當(dāng)前線程棧上的一塊空間,存儲著鎖對象原本的標(biāo)記字段垫言。它針對的是多個線程在不同時間段申請同一把鎖的情況贰剥。
偏向鎖只會在第一次請求時采用 CAS 操作,在鎖對象的標(biāo)記字段中記錄下當(dāng)前線程的地址筷频。在之后的運行過程中蚌成,持有該偏向鎖的線程的加鎖操作將直接返回。它針對的是鎖僅會被同一線程持有的情況凛捏。
-問題32: 什么是CAS的ABA問題担忧,如何避免?
-tags: java,并發(fā)
-解答:
ABA問題的根本原因在于對象值本身與狀態(tài)被畫上了等號坯癣。解決方式就是去除這個等號瓶盛,不使用值本身,而使用版本戳version做對比示罗。如java中的AtomicStampedReference惩猫。其compareAndSet
變成:
boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp)
-問題33: Lock接口實現(xiàn)的鎖和synchronized關(guān)鍵字實現(xiàn)的鎖有何不同?
-tags: java,并發(fā)
-解答:
- synchronized是Java中的關(guān)鍵字蚜点,內(nèi)置的語言實現(xiàn)帆锋;Lock是個接口,由java代碼實現(xiàn)禽额,底層數(shù)據(jù)結(jié)構(gòu)是AQS,大量使用CAS操作皮官。
- synchronized在發(fā)生異常時脯倒,會自動釋放線程占有的鎖,因此不會導(dǎo)致死鎖現(xiàn)象發(fā)生捺氢;Lock在發(fā)生異常時藻丢,如果沒有主動釋放鎖,則很可能造成死鎖現(xiàn)象摄乒,因此使用Lock時需要在finally塊中釋放鎖悠反。
- Lock可以讓等待鎖的線程響應(yīng)中斷残黑,而synchronized卻不行,使用synchronized時斋否,等待的線程會一直等待下去梨水,不能夠響應(yīng)中斷。
- Lock可以嘗試非阻塞地獲取鎖茵臭,能獲取就獲取疫诽,無法獲取就立刻返回;可以超時地獲取鎖旦委,在指定時長內(nèi)無法獲取鎖就返回奇徒。
-問題34: CountDownLatch和CyclicBarrier的使用有何區(qū)別?
-tags: java,并發(fā)
-解答:
它們都是阻塞一些行為直至某個事件發(fā)生缨硝,但Latch是等待某個事件發(fā)生摩钙,而Barrier是等待線程。
閉鎖(Latch)就像一個大門查辩,未到達(dá)結(jié)束狀態(tài)相當(dāng)于大門緊閉胖笛,不讓任何線程通過。
而到達(dá)結(jié)束狀態(tài)后宜肉,大門敞開匀钧,讓所有的線程通過,但是一旦敞開后不會再關(guān)閉谬返。
閉鎖可以用來確保一些活動在某個事件發(fā)生后執(zhí)行之斯。
我們可以用柵欄(Barrier)將一個問題分解成多個獨立的子問題,并在執(zhí)行結(jié)束后在同一處進(jìn)行匯集遣铝。
當(dāng)線程到達(dá)匯集地后調(diào)用await佑刷,await方法會阻塞直至其他線程也到達(dá)匯集地。
如果所有的線程都到達(dá)就可以通過柵欄酿炸,也就是所有的線程得到釋放瘫絮,而且柵欄也可以被重新利用。
總之填硕,Latch是聽口令行動麦萤,Barrier是看人數(shù)行動。
-問題35: SynchronousQueue的特性
-tags: java,并發(fā)
-解答:
SynchronousQueue與其他BlockingQueue有著不同特性:
- SynchronousQueue沒有容量扁眯。SynchronousQueue是一個不存儲元素的BlockingQueue壮莹。每一個put操作必須要等待一個take操作,否則不能繼續(xù)添加元素姻檀,反之亦然命满。
- 因為沒有容量,所以對應(yīng) peek, contains, clear, isEmpty … 等方法其實是無效的绣版。例如clear是不執(zhí)行任何操作的胶台,contains始終返回false,peek始終返回null歼疮。
- SynchronousQueue分為公平和非公平,默認(rèn)情況下采用非公平性訪問策略诈唬,即先來的卻后被匹配韩脏。
顯示,這是特殊的生產(chǎn)者-消費者模式讯榕。一個生產(chǎn)線程骤素,當(dāng)它生產(chǎn)產(chǎn)品(即put的時候),如果當(dāng)前沒有人想要消費產(chǎn)品(即當(dāng)前沒有線程執(zhí)行take)愚屁,此生產(chǎn)線程必須阻塞济竹,等待一個消費線程調(diào)用take操作,take操作將會喚醒該生產(chǎn)線程霎槐,同時消費線程會獲取生產(chǎn)線程的產(chǎn)品送浊。
SynchronousQueue的一個使用場景是在線程池里。Executors.newCachedThreadPool()就使用了它丘跌。目的就是保證“對于提交的任務(wù)袭景,如果有空閑線程,則使用空閑線程來處理闭树;否則新建一個線程來處理任務(wù)”耸棒。
SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue
-問題36: 什么是FutureTask,有什么用报辱?
-tags: java,并發(fā)
-解答:
FutureTask除了實現(xiàn)Future接口与殃,還實現(xiàn)了Runnable接口。因此碍现,可以交給Executor執(zhí)行幅疼,也可以由調(diào)用的線程直接執(zhí)行(FutureTask.run())。FutureTask還可以確保即使調(diào)用了多次run方法昼接,它都只會執(zhí)行一次任務(wù)爽篷。
常用的場景是在高并發(fā)環(huán)境下確保任務(wù)只執(zhí)行一次。
舉一個例子慢睡,假設(shè)有一個帶key的連接池逐工,當(dāng)key存在時,即直接返回key對應(yīng)的對象漂辐;當(dāng)key不存在時泪喊,則創(chuàng)建連接。
在高并發(fā)的情況下有可能出現(xiàn)Connection被創(chuàng)建多次的現(xiàn)象(想想如何出現(xiàn)者吁?)。創(chuàng)造性的解決思路是饲帅,當(dāng)key不存在時复凳,不是先創(chuàng)建Connection再放到Pool中瘤泪,而只是把這個“意愿”表達(dá)出來,并放到connectionPool之后執(zhí)行育八。
public Connection getConnection(String key) throws Exception {
FutureTask<Connection> connectionTask = connectionPool.get(key);
if (connectionTask != null) {
return connectionTask.get();
} else {
Callable<Connection> callable = new Callable<Connection>() {
@Override
public Connection call() throws Exception {
// 耗時的同步創(chuàng)建連接方法
return createConnection();
}
};
FutureTask<Connection> newTask = new FutureTask<Connection>(callable);
connectionTask = connectionPool.putIfAbsent(key, newTask);
if (connectionTask == null) {
connectionTask = newTask;
connectionTask.run();
}
return connectionTask.get();
}
}
核心是connectionPool.get
不再直接返回Connection
对途,而是返回FutureTask<Connection>
。
如果為空髓棋,不是直接新建連接实檀,而是通過callable
表達(dá)這個意愿,這是非常廉價快速的操作按声。哪怕因并發(fā)膳犹,兩個線程都表達(dá)了這個意愿也沒有關(guān)系,因為putIfAbsent保證只有一個意愿會被保存签则。設(shè)置這個意愿的線程會觸發(fā)run
须床,其余的都在get
處阻塞,直到run
結(jié)束渐裂。
-問題37: Volatile關(guān)鍵字的特性
-tags: java,并發(fā)
-解答:
volatile修飾的變量具有下列特性:
- 可見性豺旬。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最新寫入柒凉。JMM是這樣實現(xiàn)的:對于任意一個變量族阅,一旦修改,立即把本地內(nèi)存更新回主內(nèi)存膝捞。對于任意一個變量坦刀,一旦讀取,理解把本地內(nèi)存中置為無效绑警,從主內(nèi)存中獲取該值并更新到本地內(nèi)存求泰。
- 原子性:對任意單個volatile變量的讀/寫具有原子性。對于long和double型變量计盒,在32位系統(tǒng)中渴频,是需要分兩步讀取的,因此聲明為volatile后可實現(xiàn)原子讀缺逼簟(64位系統(tǒng)卜朗,本來就是原子讀取)咕村。需要注意的是场钉,類似于volatile++這種復(fù)合操作不具有原子性,包含了瀆與寫的操作懈涛,但是在讀寫的中間過程是沒有進(jìn)行同步的逛万,有可能被其他線程插入。
考慮下面代碼:
instance = new Singleton()
這里看起來是一句話批钠,但實際上它并不是一個原子操作宇植。
這句話被編譯成8條匯編指令得封,大致做了3件事情:
1.給Singleton的實例分配內(nèi)存。
2.初始化Singleton的構(gòu)造器
3.將instance對象指向分配的內(nèi)存空間(注意到這步instance就非null了)指郁。
第二點和第三點的順序是無法保證的忙上,也就是說,執(zhí)行順序可能是1-2-3也可能是1-3-2闲坎,如果是后者疫粥,并且在3執(zhí)行完畢、2未執(zhí)行之前腰懂,被切換到線程二上梗逮,這時候instance因為已經(jīng)在線程一內(nèi)執(zhí)行過了第三點,instance已經(jīng)是非空了悯恍,所以線程二直接拿走instance库糠,然后使用,然后順理成章地報錯涮毫。
用volatile后瞬欧,保證了instance變量的原子性,禁止把3重排序到前面罢防,即禁止volatile變量賦值之前的重排序艘虎。
-問題38: 什么時候需要自定義類加載器?
-tags: java
-解答:
我們需要的類不一定存放在已經(jīng)設(shè)置好的classPath下(有系統(tǒng)類加載器AppClassLoader加載的路徑)咒吐,對于自定義路徑中的class類文件的加載野建,我們需要自己的ClassLoader
有時我們不一定是從類文件中讀取類,可能是從網(wǎng)絡(luò)的輸入流中讀取類恬叹,還可能需要做一些加密和解密操作候生,這就需要自己實現(xiàn)加載類的邏輯,當(dāng)然其他的特殊處理也同樣適用绽昼。
可以定義類的實現(xiàn)機(jī)制唯鸭,實現(xiàn)類的熱部署,如OSGi中的bundle模塊就是通過實現(xiàn)自己的ClassLoader實現(xiàn)的硅确。