2020后端面試桩砰,java篇

2019.12-2020.02后端面試材料分享拓春,架構(gòu)/中間件篇。

拿到了字節(jié)offer亚隅,走完了Hello單車和達(dá)達(dá)的面試流程(沒給offer)硼莽,螞蟻的前三輪(接了字節(jié)Offer,放棄后續(xù)流程)煮纵。

以下問題匯總在一個類anki的小程序:一進(jìn)制懂鸵。默認(rèn)隱藏答案,思考后再點開對照行疏;根據(jù)你反饋的難度匆光,安排復(fù)習(xí)時間。

"一進(jìn)制"酿联,java篇

-問題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ù)組元素由UnsafegetObjectVolatile保證可見性,元素內(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 加上鎖往史,然后計算ConcurrentHashMapsize返回。

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 AccessedMost 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ū)(分別叫fromto)醇滥,默認(rèn)大小比例為8:1。

pic

一般情況下超营,新創(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é)容达,也就是說古涧,跟這些類型的值域相吻合。

無論在哪里花盐,longdouble類型都占用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é)果的一份拷貝遗契。在熱點代碼中頻率使用它或類似的getMethodsgetDeclaredMethods會帶來較多的堆空間消耗。

反射調(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
  • EntryThreadLocalkey乎婿,以想要存儲的對象為value
pic

再看下圖:

pic
  • Entry使用ThreadLocal的弱引用作為 Key街佑,如果ThreadLocal沒有外部強(qiáng)引用來引用它(如圖中的Thread Local Ref為null)谢翎,那么系統(tǒng) GC 時,這個ThreadLocal會被回收沐旨。
  • 這樣一來森逮,ThreadLocalMap中就會出現(xiàn)keynullEntry,就沒有辦法訪問這些Entryvalue磁携,如果這個線程遲遲不結(jié)束褒侧,就造成內(nèi)存泄漏了。

這么分析似乎是“弱引用”導(dǎo)致了泄漏谊迄,其實不是闷供。事實上,如果是強(qiáng)引用统诺,情況只會更嚴(yán)重歪脏,弱引用試圖減少泄漏的可能,只不過由于ThreadLocalMap的生命周期跟Thread一樣長粮呢,如果沒有手動刪除對應(yīng)key就會導(dǎo)致內(nèi)存泄漏婿失。

其實,對于keynullEntry啄寡,下一次ThreadLocalMap調(diào)用set豪硅、getremove的時候會被清除这难。所以真正需要做的是在不需要再用到該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)心出錯:
// 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é)碼將包含monitorentermonitorexit指令样勃。這兩種指令均會使用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)的硅确。


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末目溉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子菱农,更是在濱河造成了極大的恐慌缭付,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件循未,死亡現(xiàn)場離奇詭異陷猫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門绣檬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舅巷,“玉大人,你說我怎么就攤上這事河咽。” “怎么了赋元?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵忘蟹,是天一觀的道長。 經(jīng)常有香客問我搁凸,道長媚值,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任护糖,我火速辦了婚禮褥芒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嫡良。我一直安慰自己锰扶,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布寝受。 她就那樣靜靜地躺著坷牛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪很澄。 梳的紋絲不亂的頭發(fā)上京闰,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機(jī)與錄音甩苛,去河邊找鬼蹂楣。 笑死,一個胖子當(dāng)著我的面吹牛讯蒲,可吹牛的內(nèi)容都是我干的痊土。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼爱葵,長吁一口氣:“原來是場噩夢啊……” “哼施戴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起萌丈,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤赞哗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辆雾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肪笋,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了藤乙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猜揪。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖坛梁,靈堂內(nèi)的尸體忽然破棺而出而姐,到底是詐尸還是另有隱情,我是刑警寧澤划咐,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布拴念,位于F島的核電站,受9級特大地震影響褐缠,放射性物質(zhì)發(fā)生泄漏政鼠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一队魏、第九天 我趴在偏房一處隱蔽的房頂上張望公般。 院中可真熱鬧,春花似錦胡桨、人聲如沸官帘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遏佣。三九已至,卻和暖如春揽浙,著一層夾襖步出監(jiān)牢的瞬間状婶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工馅巷, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留膛虫,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓钓猬,卻偏偏與公主長得像稍刀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子敞曹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,490評論 2 348

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

  • 第二部分 自動內(nèi)存管理機(jī)制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運行數(shù)據(jù)區(qū)域 程序計數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,136評論 0 2
  • 一账月、運行時數(shù)據(jù)區(qū)域 Java虛擬機(jī)管理的內(nèi)存包括幾個運行時數(shù)據(jù)內(nèi)存:方法區(qū)、虛擬機(jī)棧澳迫、本地方法棧局齿、堆、程序計數(shù)器橄登,...
    加油小杜閱讀 1,514評論 1 15
  • 本系列出于AWeiLoveAndroid的分享抓歼,在此感謝讥此,再結(jié)合自身經(jīng)驗查漏補(bǔ)缺,完善答案谣妻。以成系統(tǒng)萄喳。 Java基...
    濟(jì)公大將閱讀 1,524評論 1 6
  • Java SE 基礎(chǔ): 封裝、繼承蹋半、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務(wù))結(jié)合為一個獨立的整體他巨,并盡...
    Jayden_Cao閱讀 2,103評論 0 8
  • 所有知識點已整理成app app下載地址 J2EE 部分: 1.Switch能否用string做參數(shù)? 在 Jav...
    侯蛋蛋_閱讀 2,412評論 1 4