【備戰秋招】【併發】Java併發編程面試150問

源文件地址:https://github.com/2020GetGoodOffer/test


Java併發編程面試150問
Q1:線程越多程序是否就運行得越快?
答:併發編程的目的是爲了讓程序運行得更快,但是並不是啓動得線程越多就能讓程序最大限度地併發執行。在併發編程時,如果希望通過多線程執行任務讓程序運行得更快會面臨很多挑戰,比如上下文切換的問題、死鎖的問題,以及受限於硬件和軟件的資源限制問題。

Q2:多線程併發是怎麼實現的,必須要用多核處理器實現嗎?
答:即使是單核處理器也支持多線程執行代碼,CPU通過給每個線程分配CPU時間片來實現這個機制。時間片是CPU分配給各個線程的時間,因爲時間片非常短(一般是幾十毫秒),所以CPU通過不停地切換線程執行,讓我們感覺多個線程是同時執行的。

Q3:什麼是上下文切換?
答:CPU是通過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。但是在切換前會保存上一個任務的狀態,以便下次再切換回這個任務時可以再加載這個任務的狀態。所以任務從保存到再加載的過程就是一次上下文切換。

Q4:如何減少上下文切換?
答:①無鎖併發編程:多線程競爭鎖時會引起上下文切換,所以多線程處理數據時,可以通過一些方法來避免使用鎖,例如將數據的id按照hash算法取模分段,不同的線程處理不同數據段的數據。②CAS算法:Java的atomic包使用CAS算法來更新數據而不需要加鎖。③使用最少線程:避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態。④協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。

Q5:多線程避免死鎖的方法?
答:①避免一個線程同時獲得多個鎖。②避免一個線程在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源。③嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。④對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會出現解鎖失敗的問題。

Q6:volatile關鍵字的作用?
答:①volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的可見性。可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。②如果volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因爲它不會引起線程上下文的切換和調度。③如果一個字段被聲明成volatile,Java線程內存模型確保所有線程看到這個變量的值是一樣的。

Q7:volatile的底層是如何實現的?
答:有volatile修飾的共享變量在進行寫操作時的彙編代碼是具有lock前綴的指令,lock
前綴的指令在多核處理器下會引發兩件事:①將當前處理器緩存行的數據寫回到系統內存。②處理器將緩存回寫到內存的操作會使在其他CPU裏緩存了該內存地址的數據無效。
爲了提高處理速度,處理器不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存後再進行操作,但操作完不知道何時會寫回內存。如果對聲明瞭volatile的變量進行寫操作,JVM就會向處理器發送一條lock前綴的指令,將這個變量在緩存行的數據寫回到系統內存。但是就算寫回內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題(ABA問題)。所以在多處理器下,爲了保證各個處理器的緩存是一致的就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置爲無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存裏。

Q8:volatile如何優化性能?
答:可以通過追加字節的方式優化性能,例如JDK7中的隊列集合類LinkedTransferQueue就是使用了追加字節的方式來優化隊列出隊和入隊的性能。由於一些處理器的高速緩存行是64個字節寬,不支持部分填充緩存行,如果隊列的頭節點和尾節點都不足64字節,當一個處理器試圖修改頭節點時就會將整個緩存行鎖定,那麼在緩存一致性的作用下會導致其他處理器不能訪問自己高速緩存中的尾節點,而隊列的入隊和出隊又會頻繁修改頭節點和尾節點,因此多處理器情況下會嚴重影響隊列的入隊和出隊效率。追加到64字節後就可以填滿高速緩衝區的緩存行,避免頭節點和尾節點加載到同一個緩存行,使它們的操作不會互相鎖定。
但以下兩種場景不應該使用這種方式:①緩存行非64字節寬的處理器。②共享變量不會被頻繁地寫,因爲使用追加字節的方式需要處理器讀取更多的字節到高速緩衝區,這本身就會帶來一定性能消耗。如果共享變量不被頻繁寫,鎖的機率很小沒有必要避免互相鎖定。不過這種追加字節的方式在Java7可能不生效,因爲Java7可以淘汰或重新排列無用字段,需要使用其他追加字節的方式。

Q9:synchronized鎖的形式有哪些?
答:①對於同步普通方法,鎖是當前實例對象。②對於靜態同步方法,鎖是當前類的Class對象。③對於同步方法塊,鎖是synchronized括號裏配置的對象。

Q10:synchronized的底層是怎麼實現的?
答:JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用另一種方式實現的,細節並未在JVM規範中詳細說明,但是方法的同步也可以使用這兩個指令來實現。
monitorenter指令是編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有monitorexit與之配對。任何對象都有一個monitor與之關聯,當一個monitor被持有後它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。

Q11:什麼是鎖升級(鎖優化)?
答:JDK1.6爲了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖,在JDK1.6中,鎖一共有4個狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級,如果偏向鎖升級成輕量級鎖後就不能降級成偏向鎖,這種只能升級不能降級的鎖策略是爲了提高獲得鎖和釋放鎖的效率。

Q12:偏向鎖的獲得原理是什麼?
答:大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步代碼塊並獲取鎖時,會在對象頭和幀棧中的鎖記錄裏存儲鎖偏向的線程ID,以後該線程再進入和退出同步代碼塊不需要進行CAS操作來加鎖和解鎖,只需要簡單地測試一下對象頭(synchronized用的鎖存在Java的對象頭裏)的Mark Word裏是否存儲着指向當前線程的偏向鎖。
如果測試成功表示線程已經獲得了鎖,如果測試失敗則需要再測試一下Mark Word(主要存儲鎖狀態、對象的hashCode、對象的分代年齡、是否是偏向鎖、鎖標誌位)中偏向鎖的標識是否設置成了1(表示當前是偏向鎖),如果設置了就嘗試使用CAS將對象頭的偏向鎖指向當前線程,否則使用CAS競爭鎖。

Q13:偏向鎖的撤銷原理是什麼?
答:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(該時間點上沒有正在執行的字節碼),它會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活着,如果線程不處於活動狀態則將對象頭設爲無鎖狀態;如果線程還活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼重新偏向於其他線程,要麼恢復到無鎖或者標記對象不適合作爲偏向鎖,最後喚醒暫停的線程。

Q14:偏向鎖的打開和關閉是怎麼實現的?
答:偏向鎖在Java6和Java7中默認是開啓的,但是它在應用程序啓動幾秒後才激活,如果有必要可以使用JVM參數來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應用程序裏所有的鎖通常情況處於競爭狀態,可以通過JVM參數來關閉偏向鎖:-XX:UseBiasedLocking=false,那麼程序默認會進入輕量級鎖狀態。

Q15:輕量級鎖的加鎖原理是什麼?
答:線程在執行同步塊之前,JVM會先在當前線程的棧幀中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,稱爲Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針,如果成功那麼當前線程獲得鎖,如果失敗表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

Q16:輕量級鎖的解鎖原理是什麼?
答:輕量級鎖解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功則表示沒有競爭發生。如果失敗則表示當前存在鎖競爭,鎖就會膨脹爲重量級鎖。
因爲自旋會消耗CPU,爲了避免無用的自旋(比如獲得鎖的線程被阻塞了),一旦鎖升級爲重量級鎖,就不會再恢復到輕量級鎖的狀態。在這種情況下,其他線程視圖獲取鎖時都會被阻塞,當持有鎖的線程釋放鎖後纔會喚醒這些線程,被喚醒的線程就會對鎖資源進行新一輪的爭奪。

Q17:偏向鎖、輕量級鎖和重量級鎖的區別?
答:①偏向鎖的優點是加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距,缺點是如果線程間存在鎖競爭會帶來額外鎖撤銷的消耗,適用於只有一個線程訪問同步代碼塊的場景。②輕量級鎖的優點是競爭的線程不會阻塞,提高了程序的響應速度,缺點是如果線程始終得不到鎖會自旋消耗CPU,適用於追求響應時間和同步代碼塊執行非常快的場景。③重量級鎖的優點是線程競爭不使用自旋不會消耗CPU,缺點是線程會被阻塞,響應時間很慢,適應於追求吞吐量,同步代碼塊執行較慢的場景。

Q18:原子操作是什麼,處理器是怎麼實現原子操作的?
答:原子操作即不可被中斷的一個或一系列操作,處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操作的原子性。
①通過總線鎖定保證原子性:如果多個處理器同時對共享變量進行讀改寫操作(例如i++),那麼共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子性的,操作完之後共享變量的值會和期望的不一樣。例如i=1,進行兩次i++操作,但是結果可能爲2。這是因爲多個處理器同時從各自的緩存讀取變量i,分別進行加1操作,然後分別寫入系統內存中。如果想要保證讀改寫操作的原子性,就必須保證CPU1讀改寫共享變量時CPU2不能操作緩存了該共享變量內存地址的緩存。處理器使用總線鎖來解決這個問題,總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞,該處理器就可以獨佔共享內存。
②通過緩存鎖定來保證原子性:同一時刻只需要保存對某個內存地址的訪問是原子性即可,但總線鎖定把CPU和內存之間的通信鎖住了,這使得鎖定期間其他處理器不能操作其他內存地址的數據,開銷比較大,目前的處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。頻繁使用的內存會緩存在處理器的高速緩存裏,原子操作就可以直接在處理器內部緩存中進行,並不需要聲明總線鎖。緩存鎖定是指內存區域如果被緩存在處理器的緩存行中並且在Lock操作期間被鎖定,那麼當它執行鎖操作回寫內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並允許它的緩存一致性機制來保證操作原子性,因爲緩存一致性會阻止同時修改由兩個以上處理器緩存的內存區域,當其他處理器回寫已被鎖定的緩存行數據時會使緩存行無效。

Q19:不會使用緩存鎖定的情況有哪些?
答:①當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行時,處理器會調用總線鎖定。②有些處理器不支持緩存鎖定,例如Intel486和Pentium處理器,即使鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。

Q20:緩存行和CAS是什麼?
答:①緩存行:緩存的最小操作單位。②CAS:Compare and Swap,比較並交換,CAS需要兩個數值,一個是舊值(期望操作前的值)和一個新值,在操作期間先比較舊值有沒有發生變化,如果沒有發生變化才交換成新值,發生了變化則不交換。

Q21:Java中如何實現原子操作?
答:Java中可以通過鎖和循環CAS的方式來實現原子操作。
鎖機制保證了只有獲得鎖的線程才能操作鎖定的內存區域,JVM內部實現了很多鎖,除了偏向鎖JVM實現鎖的方式都用了循環CAS,即當一個線程想進入同步代碼塊時使用循環CAS方式獲取鎖,退出時使用循環CAS釋放鎖。
JVM中的CAS操作利用了處理器提供的交換指令CMPXCHG實現,自旋CAS的基本思路就是循環進行CAS操作直到成功爲止。從Java1.5開始JDK的併發包裏提供了一些類來支持原子操作,例如AtomicBoolean(用原子方式更新的boolean值),AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值),這些原子包裝類還提供了有用的工具方法,比如以原子的方式將當前值自增1和自減1。

Q22:CAS實現原子操作有什麼問題?
答:①ABA問題:因爲CAS需要在操作值的時候檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成B,又變成了A,那麼使用CAS檢查時會發現它的值並未發生變化。ABA問題的解決思路就是使用版本號,在變量前面追加版本號,每次更新時把版本號加1,那麼A->B->A就會i變成1A->2B->3A。從Java1.5開始,JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題,這個類的compareAndSet方法首先檢查當前引用是否等於預期引用,並且檢查當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和標誌的值設置爲給定的更新值。
②循環時間長,開銷大:自旋CAS如果長時間不成功會給CPU帶來非常大的執行開銷。如果JVM支持處理器提供的pause指令,就可以提升效率,因爲pause指令可以延遲流水線執行指令,使CPU不會消耗過多的執行資源,延遲的時間取決於具體版本;避免在退出循環時因爲內存順序衝突而引起CPU流水線被清空,從而提高CPU的執行效率。
③只能保證一個共享變量的原子操作:循環CAS不能保證多個共享變量操作的原子性,可以使用鎖或把多個共享變量合成爲一個,放在一個對象裏進行CAS操作。

Q23:Java中線程是如何通信和同步的?
答:通信是指線程之間以何種機制來交換信息,在命令式編程中線程之間的通信機制有兩種,共享內存和消息傳遞。在共享內存的併發模型裏線程之間共享程序的公共狀態,通過寫-讀內存中的公共狀態進行隱式通信。在消息傳遞的併發模型裏線程之間沒有公共狀態,線程之間必須通過發送消息來顯示通信。
同步是指程序中用於控制不同線程間操作發生相對順序的機制,在共享內存的併發模型裏同步是顯示進行的,程序員必須顯示指定某個方法或代碼需要在線程之間互斥執行。在消息傳遞的併發模型裏,由於消息的發送必須在接受之前,同步是隱式進行的。
Java併發採用共享內存模型,線程之間的通信總是隱式進行,整個通信過程對程序員完全透明。

Q24:哪些數據會存在內存可見性問題?
答:在Java中,所有實例域、靜態域和數組元素都存儲在堆中,堆內存在線程之間共享,因此這些共享變量存在內存可見性問題。局部變量、方法定義參數和異常處理器參數等不會在線程之間共享,不會存在內存可見性問題,也不受內存模型的影響。

Q25:JMM是什麼?
答:JMM(Java Memory Model)是Java內存模型,Java線程之間的通信由JMM控制,JMM決定了一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器以及其他的硬件和編譯器優化。 兩個線程之間的通信必須經過主內存,JMM通過控制主內存與每個線程的本地內存之間的交互來爲Java程序員提供內存可見性保證。

Q26:指令重排序是什麼?
答:重排序指從源代碼到指令序列的重排序,在執行程序時爲了提高性能,編譯器和處理器通常會對指令進行重排序,重排序分爲三種類型。
①編譯器優化的重排序:編譯器在不改變單線程程序語義的前提下可以重新安排語句的執行順序。②指令級並行的重排序:現代處理器才以來指令級並行技術ILP來將多條指令重疊執行,如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。③內存系統的重排序:由於處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操作操作看上去可能是亂序執行。

Q27:指令重排序的問題及解決?
從Java源代碼到最終實際執行的指令序列,會分別經歷編譯器優化重排序、指令級並行重排序和內存系統重排序。這些重排序可能會導致多線程程序出現內存可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序。對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障指令(一組用於實現對內存操作順序限制的處理器指令),通過內存屏障指令來禁止特定類型的處理器重排序。
JMM屬於語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的可見性內存保證。

Q28:JMM內存屏障指令的分類有哪些?
答:①Load Load,確保Load1的數據裝載先於Load2及所有後續裝載指令的裝載。②Store Store,確保Store1數據對其他處理器可見(刷新到內存)先於Store2及所有後續存儲指令的存儲。③Load Store,確保Load1數據裝載先於Store2及所有後續存儲指令刷新到內存。④Store Load,確保Store1數據對其他處理器變得可見(刷新到內存)先於Load2及所有後續裝載指令的裝載。Store Load會使該屏障之間的所有內存訪問指令(存儲和裝載指令)完成之後才執行該屏障之後的內存訪問指令。該指令是一個“全能型”屏障,同時具備其他三個屏障的效果,現代的多處理器大多支持該屏障,執行該屏障的開銷很昂貴,因爲當前處理器通常要把寫緩衝區的數據全部刷新到內存中。

Q29:happens-before是什麼?
答:從JDK5開始,Java使用新的JSR-133內存模型,JSR-133使用happens-before的概念來闡述操作之間的內存可見性。在JMM中如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係,這兩個操作既可以是在一個線程之內,也可以是不同線程之內。
與程序員密切相關的happens-before規則如下:①程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作。②監視器鎖規則:對一個鎖的解鎖,happens-before於隨後這個鎖的加鎖。③volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。④傳遞性,如果A happens-before B,且B happens-before C,那麼A happens-before C。⑤start()規則,如果線程A執行操作ThreadB.start()(啓動線程B),那麼A線程的ThreadB.start()操作happens-before於線程B中的任意操作。⑥join()規則,如果線程A執行操作ThreadB.join()併成功返回,那麼線程B中的任意操作happens-before於線程A從ThreadB.join()操作成功返回。
注意:兩個操作之間具有happens-before關係,並不意味着前一個操作必須要在後一個操作之前執行。happens-before僅僅要求前一個操作(執行的結果)對後一個操作可見,且前一個操作按順序排在第二個操作之前。

Q30:什麼是數據依賴性?
答:如果兩個操作訪問同一個變量,且這兩個操作中有一個爲寫操作,此時兩個操作之間就存在數據依賴性。數據以來分爲以下三種類型:①寫後讀,寫一個變量之後再讀這個位置。②讀後寫,讀一個變了之後再寫這個變量。③寫後寫,寫一個變量之後再寫這個變量。
上述三種情況只要重排序兩個操作的執行順序,程序的執行結果就會被改變。編譯器和處理器爲了性能優化可能會對操作重排序,在重排序時會遵守數據依賴性,不會改變存在數據依賴關係的兩個操作的執行順序。這裏說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。

Q31:as-if-serial語義是什麼?
答:as-if-serial指不管怎麼重排序(編譯器和處理器爲了提高並行度),(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵循該語義。爲了遵循該語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因爲這種重排序會改變執行結果。但如果操作之間不存在數據依賴關係,這些操作就可能被重排序。
as-if-serial語義將單線程保護了起來,遵循as-if-serial語義的編譯器、runtime和處理器共同爲編寫單線程程序的程序員創建了一個幻覺:單線程程序是按照程序的順序執行的。as-if-serial使單線程程序員無需擔心重排序會干擾他們,也無需擔心內存可見性問題。
as-if-serial實例:例如計算一個圓的面積,A操作給半徑賦值,B操作給圓周率賦值,C操作計算圓的面積。由於C依賴於A和B因此不會被重排到A和B的前面,但A和B之間沒有數據依賴關係,所以程序的執行順序可以是ABC或BAC,結果是一樣的。

Q32:控制依賴關係對指令重排序的影響?
答:當代碼中存在控制依賴性時(例如A操作判斷某標誌位,B操作根據A的結果執行對應邏輯),會影響指令序列執行的並行度。爲此編譯器和處理會採用猜測執行來克服控制相關性對並行度的影響,可以提前計算出值保存到名爲重排序緩衝的硬件緩存中,如果之前的控制條件滿足就執行對應操作。
在單線程程序中,對存在控制依賴的操作重排序並不會改變程序的執行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因)。但在多線程程序中,對存在控制依賴的操作從排序可能會改變程序的執行結果。

Q33:數據競爭和順序一致性是什麼?
答:當程序未正確同步時就可能存在數據競爭。JMM規範對數據競爭的定義如下:在一個線程中寫一個變量,在另一個線程中讀同一個變量,而且寫和讀沒有通過同步來排序。當代碼中包含數據競爭時程序的執行往往產生違反直覺的結果,如果一個多線程程序能正確同步,這個程序將是一個沒有數據競爭的程序。
JMM對正確同步的多線程程序的內存一致性做了如下保證:如果程序是正確同步的,程序的執行將具有順序一致性,即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。這裏的同步是指廣義上的同步,包括對常用同步用語(synchronized、volatile和final)的正確使用。

Q34:順序一致性內存模型的特點?
答:順序一致性內存模型是一個理想化的理論參考模型,它爲程序員提供了極強的內存可見性保證。順序一致性內存模型有兩大特性:①一個線程中的所有操作必須按照程序的順序來執行。②不管程序是否同步,所有線程都只能看到一個單一的操作執行順序,在順序一致性的內存模型中,每個操作都必須原子執行並且立即對所有線程可見。
在概念上,順序一致性模型有一個單一的全局內存,這個內存通過一個左右擺動的開關可以連接到任意一個線程,同時每一個線程必須按照程序的順序來執行內存讀/寫操作。在任意時間點最多只能有一個線程可以連接到內存,當多個線程併發執行時,開關裝置能把線程的所有內存讀/寫操作串行化(即在順序一致性模型中所有操作之間具有全序關係)。

Q35:未同步程序在JMM中的問題?
答:未同步程序在順序一致性模型中雖然整體執行順序無序但是所有線程都能看到一個一致的整體執行順序。之所以能得到這個保證是因爲順序一致性內存模型中的每個操作必須立即對任意線程可見。
JMM中沒有這個保證,未同步程序在JMM中不但整體的執行順序無序,並且所有線程看到的操作執行順序也可能不一致。比如當前線程把寫過的數據緩存到本地內存,在沒有刷新到主內存前,這個寫操作僅對當前線程可見。從其他線程的角度會認爲這個寫操作並沒有執行,只有當前線程把本地內存中寫過的數據刷新回主內存之後這個寫操作纔對其他線程可見,這種情況下當前線程和其他線程看到的操作執行順序不一致。

Q36:未同步程序的執行特性?
答:對於未同步或未正確同步的多線程程序,JMM只提供最小安全性:線程執行時讀取到的值要麼是之前某個線程寫入的值,要麼是默認值,JMM保證線程讀操作讀取到的值不會無中生有。爲了實現最小安全性,JVM在堆上分配對象時首先會對內存空間進行清零,然後纔會在上面分配對象(JVM內部同步這兩個操作)。因此在已清零的內存空間分配對象時,域的默認初始化已經完成了。
JMM不保證未同步程序的執行結果與該程序在順序一致性模型的執行結果一致,因爲如果想要保證一致需要禁止大量的處理器和編譯器優化,這對程序執行性能會有很大影響。而且未同步程序在順序一致性模型中執行時整體是無序的,結果無法預知,因此保證未同步執行程序在兩個模型的執行結果一致沒什麼意義。

Q37:未同步程序在JMM和順序一致性模型的執行區別?
答:①順序一致性模型保證單線程內的操作會按程序的順序執行,而JMM不保證單線程內的操作會按程序的順序執行。
②順序一致性模型保證所有線程只能看到一致的操作執行順序,而JMM不保證所有線程能看到一致的操作執行順序。
③JMM不保證對64位的long類型和double類型變量的寫操作具有原子性,而順序一致性模型保證對所有的內存讀/寫操作都具有原子性。

Q38:總線的工作機制是什麼?
答:在計算機中,數據通過總線在處理器和內存之間傳遞。每次處理器和內存之間的數據傳遞都是通過一系列步驟來完成的,這一系列步驟稱爲總線事務。總線事務包括讀事務和寫事務。讀事務從內存中傳輸數據到處理器,寫事務從處理器傳送數據到內存,每個事務會讀/寫內存中的一個或多個物理上連續的字,總線會同步試圖併發使用總線的事務。在一個處理器執行總線事務期間,總線會禁止其他的處理器和IO設備執行內存的讀/寫。

Q39:總線工作機制的好處?
答:總線的工作機制可以把所有處理器對內存的訪問以串行化的方式來執行,在任意時間點最多只能有一個處理器訪問內存,這個特性確保了單個總線事務之中的內存讀/寫操作具有原子性。

Q40:long和double變量的原子性問題?
答:在一些32位的處理器上如果要求對64位數據的寫操作具有原子性,會有比較大的開銷。爲了照顧這種處理器,Java語義規範鼓勵但不強求JVM對64位的long和double類型變量的寫操作具有原子性,當JVM在這種處理器上運行時可能會把一個64位的long/double變量寫操作拆分爲兩個32位的寫操作執行,這兩個32位的寫操作可能會被分配到不同的總線事務中執行,此時對這個64位的寫操作不具有原子性。
在JSR-133之前的舊內存模型允許把一個64位的double/long變量的讀/寫操作拆分位兩個64位的讀/寫操作執行。從JSR-133內存模型(JDK5)開始,僅僅只允許把一個64位的long/double類型變量寫操作拆分爲兩個32位的寫操作來執行,任意的讀操作在JSR-133中必須具有原子性(即任意讀操作必須要在單個讀事務中執行)。

Q41:volatile變量的特性?
答:①可見性:對任意一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。②原子性:對任意單個volatile變量的讀/寫具有原子性,但類似於i++這種複合操作不具有原子性。

Q42:volatile變量的內存語義?
答:從JSR-133開始,volatile變量的寫-讀可以實現線程之間的通信。從內存語義的角度來說,volatile的寫-讀與鎖的釋放-獲取具有相同的內存效果。
volatile寫的內存語義如下:當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。volatile讀的內存語義如下:當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效,線程接下來將從主內存中讀取共享變量。
線程A寫一個volatile變量,實質上是線程A向接下來要讀這個volatile變量的某個線程發出了(其對共享變量所修改的)消息。線程B讀一個volatile變量,實質上是線程B接收了之前某個線程發出的(在寫這個volatile變量之前對共享變量所做修改的)消息。線程A寫一個volatile變量,線程B讀一個volatile變量,實質上是線程A通過主內存向線程B發送消息。

Q43:volatile指令重排序的特點?
答:①當第二個操作是volatile寫時,不管第一個操作是什麼都不能重排序,這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。
②當第一個操作是volatile讀時,不管第二個操作是什麼都不能重排序,這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
③當第一個操作是volatile寫,第二個操作是volatile讀時不能重排序。

Q44:volatile內存語義是怎麼實現的?
答:JMM通過分別限制編譯器重排序和處理器重排序來實現volatile的內存語義。編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。對於編譯器來說,發現一個最優佈置來最小化插入屏障的總數幾乎不可能,爲此JMM採取保守策略。

Q45:JMM內存屏障插入策略有哪些?
答:①在每個volatile寫操作之前插入一個Store Store屏障,禁止之前的普通寫和之後的volatile寫重排序。
②在每個volatile寫操作之後插入一個Store Load屏障,防止之前的volatile寫與之後可能有的volatile讀/寫重排序,也可以在每個volatile變量讀之前插入該屏障,考慮到一般是讀多於寫所以選擇用這種方式提升執行效率,也可以看出JMM在實現上的一個特點:首先確保正確性,然後再去追求效率。
③在每個volatile讀操作之後插入一個Load Load屏障,禁止之後的普通讀操作和之前的volatile讀重排序。
④在每個volatile讀操作之後插入一個Load Store屏障,禁止之後的普通寫操作和之前的volatile讀重排序。

Q46:JSR-133增強volatile內存語義的原因?
答:在舊的內存模型中,雖然不允許volatile變量之間重排序,但允許volatile變量與普通變量重排序,可能導致內存不可見問題。在舊的內存模型中volatile的寫-讀沒有鎖的釋放-獲取所具有的內存語義,爲了提供一種比鎖更輕量級的線程通信機制,嚴格限制了編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的釋放-獲取具有相同的內存語義。只要volatile變量與普通變量之間的重排序可能會破壞volatile的內存語義這種重排序就會被編譯器重排序規則和處理器內存屏障插入策略禁止。

Q47:鎖的內存語義?
答:當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。當線程獲取鎖時,JMM會把線程對應的本地內存置爲無效,從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。
鎖的釋放與volatile寫具有相同的內存語義,鎖獲取與volatile讀具有相同的內存語義。線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所做修改的)消息。線程B獲取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改的)消息。線程A釋放這個鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息。

Q48:鎖的內存語義是怎麼實現的?
答:公平鎖和非公平鎖釋放時,最後都要寫一個volatile變量state。公平鎖獲取鎖時,首先會去讀volatile變量,非公平鎖獲取鎖時,首先會用CAS更新volatile變量的值,這個操作同時具有volatile讀和volatile寫的內存語義。因此鎖的釋放-獲取內存語義的實現方式爲:①利用volatile變量的寫-讀具有的內存語義。②利用CAS所附帶的volatile讀和volatile寫的內存語義。

Q49:Java中concurrent包的原子性是如何保證的?
答:由於Java的CAS同時具有volatile讀和volatile寫的內存語義,因此Java線程通信有以下四種方式:①A線程寫volatile變量,隨後B線程讀這個volatile變量。②A線程寫volatile變量,隨後B線程用CAS更新該變量。③A線程用CAS更新一個volatile變量,隨後B線程用CAS更新該變量。④A線程用CAS更新一個volatile變量,隨後B線程讀這個變量。
Java的CAS會使用現代處理器上提供的高效機器級別的原子指令,這些原子指令以原子方式對內存進行讀-改-寫操作。同時volatile變量的讀/寫和CAS可以實現線程之間的通信,這些特性就是concurrent包的基石。concurrent包有一個通用的實現模式:首先聲明共享變量爲volatile,然後使用CAS的原子條件更新來實現線程之間的同步,同時配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。包括AQS,非阻塞數據結構和原子變量類這些基礎類都是通過這種模式來實現的,而concurrent包中的高層類又是依賴這些基礎類來實現的。

Q50:final域的重排序規則?
答:對於final域,編譯器和處理器要遵守兩個重排序規則:①在構造方法內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。②初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序。

Q51:寫final域重排序的實現原理?
答:寫final域的重排序規則禁止把final域的寫重排序到構造方法之外,這個規則的實現包含以下兩方面:①JMM禁止編譯器把final域的寫重排序到構造方法之外。②編譯器會在final域的寫之後,構造方法的return之前,插入一個Store Store屏障,這個屏障禁止把final域的寫重排序到構造方法之外。
寫final域的重排序可以確保在對象引用爲任意線程可見之前,對象的final域已經被正確初始化過了,而普通域不具有這個保障。

Q52:讀final域重排序的實現原理?
答:讀final域的重排序規則是,在一個線程中,初次讀對象引用和初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作(僅針對處理器)。編譯器會在讀final域操作的前面插入一個Load Load屏障。
初次讀對象引用與初次讀該對象包含的final域,這兩個操作之間存在間接依賴關係。由於編譯器遵守間接依賴關係因此編譯器不會重排序這兩個操作。大多數處理器也會遵守間接依賴,也不會重排序這兩個操作。但有少數處理器允許對存在間接依賴關係的操作做重排序(例如alpha處理器),因此該規則就是專門針對這種處理器的。
讀final域的重排序規則可以確保在讀一個對象的final域之前,一定會先讀包含這個final域的對象的引用。

Q53:final域爲引用對象時重排序的特點?
答:對於引用類型,寫final域的重排序規則對編譯器和處理器增加了如下約束:在構造方法內對一個final引用的對象的成員域的寫入,與隨後在構造方法外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。

Q54:final引用的可見性問題?
答:寫final域的排序規則可以確保在對象引用爲任意線程可見之前,該引用變量指向對象的final域已經在構造方法中被正確初始化過了。其實要實現這個保證還需要在構造方法內部,不能讓這個被構造對象的引用爲其他線程所見,也就是對象引用不能在構造方法中逸出。在構造方法返回前,被構造對象的引用不能爲其他線程所見,因爲此時的final域可能還沒有被正確地初始化。在構造方法返回後,任意線程都將保證能看到final域正確初始化後地值。
Q55:final語義在X86處理器的實現原理是什麼?
答:寫final域的重排序規則是要求編譯器在final域的寫之後,構造方法return之前插入一個Store Store屏障,讀final域的重排序規則是要求編譯器在讀final域的操作前插入一個Load Load屏障。
由於X86處理器不會對寫-寫操作重排序,所以寫final域需要的Store Store屏障會被省略。同樣,由於X86處理器不會對存在間接依賴關係的操作做重排序,所以在X86處理器中讀final域需要的Load Load屏障也會被省略掉。也就是說,X86處理器不會對final域的讀/寫插入任何內存屏障。

Q56:JSR-133增強final語義的原因?
答:在舊的Java內存模型中,一個最嚴重的缺陷就是線程可能看到final域的值會改變。比如一個線程看到一個int類型final域的值爲0(還未初始化之前的默認值),過一段時間之後這個線程再去讀這個final域的值會發現值變爲1(被某個線程初始化之後的值)。最常見的例子就是舊的Java內存模型中String的值可能會改變。
爲了修復該漏洞,JSR-133通過爲final域增加寫和讀重排序規則,可以爲Java程序員提供初始化安全保證:只要對象是正確構造的(被構造對象的引用在構造方法中沒有逸出),那麼不需要使用同步(指lock和volatile的使用)就可以保證任意線程都能看到這個final域在構造方法中被初始化之後的值。

Q57:happens-before的重排序策略?
答:JMM將happens-before要求禁止的重排序分爲了下面兩類:會改變程序執行結果的重排序和不會改變程序執行結果的重排序。JMM對這兩種不同性質的重排序採取了不同的策略,對於會改變程序執行結果的重排序JMM要求編譯器和處理器必須禁止這種重排序;對於不會改變程序執行結果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種重排序)。
JMM向程序員提供happens-before規則能滿足程序員的需求,其規則不但簡單易懂而且也向程序員提供了足夠強的內存可見性保證(有些內存保證性不一定真實存在,例如不改變執行結果的指令重排序對程序員是透明的)。
JMM對編譯器和處理器的束縛已經儘可能地少,JMM遵循一個基本原則:只要不改變程序執行結果,編譯器和處理器怎麼優化都行。例如編譯器分析某個鎖只會單線程訪問就消除該鎖,某個volatile變量只會單線程訪問就把它當作普通變量。

Q58:happens-before的具體定義是什麼?
答:①如果一個操作happens-before另一個操作,那麼第一個操作的執行結果對第二個操作可見,並且第一個操作的執行順序排在第二個操作之前。這是JMM對程序員的承諾。
②兩個操作之間存在happens-before關係,並不意味着Java平臺的具體實現必須要按照happens-before關係指定的順序執行,如果重排序之後的執行結果與按照happens-before關係的執行結果一致,那麼這種重排序是可以允許的。這是JMM對編譯器和處理器的約束規則,JMM遵循一個基本原則:只要不改變程序執行結果,編譯器和處理器怎麼優化都行。JMM這麼做的原因是程序員對於這兩個操作是否真的被重排序並不關心,程序員關心的是程序執行的語義不能被改變(即執行結果不能被改變)。因此happens-before關係的本質和as-if-serial一樣。

Q59:happens-before和as-if-serial的區別?
答:as-if-serial語義保證單線程程序的執行結果不被改變,happens-before保證正確同步的多線程程序的執行結果不被改變。as-if-serial語義給編寫單線程程序的程序員創造了一種單線程程序是順序執行的幻覺,happens-before關係給編寫正確同步的多線程程序員創造了一種多線程程序是按照happens-before指定順序執行的幻覺。這兩種語義的目的都是爲了在不改變程序執行結果的前提下儘可能提高程序執行的並行度。

Q60:happens-before規則的相關實現原理?
答:①程序順序規則:編譯器和處理器都要遵守as-if-serial語義,as-if-serial語義保證了程序順序執行規則。②volatile規則:對一個volatile變量的讀總是能看到(任意線程)之前對這個volatile變量最後的寫入,因此volatile的這個特性可以保證實現volatile規則。③傳遞性規則:由volatile的內存屏障插入策略和volatile的編譯器重排序規則共同保證。

Q61:處理器內存模型的分類?
答:①放鬆程序中寫-讀操作的順序,由此產生了TSO內存模型。②在TSO的基礎上繼續放鬆程序中寫-寫操作的順序,由此產生了PSO內存模型。③在TSO和PSO的基礎上,繼續放鬆程序中讀-寫(以兩個操作之間不存在數據依賴性爲前提)和讀-讀操作的順序,由此產生了RMO和PowerPC內存模型。

Q62:JMM對不同處理器模型的處理?
答:不同的處理器模型,性能越好,內存模型的設計越弱,因爲處理器希望內存模型對它們的束縛越少越好,這樣它們就可以做儘可能多的優化來提高性能。由於常見的處理器內存模型比JMM要弱,Java編譯器在生成字節碼時,會在執行指令序列的適當位置插入內存屏障來限制處理器的重排序。同時由於各種處理器內存模型的強弱不同,爲了在不同的處理器平臺向程序員展示一個一致的內存模型,JMM在不同的處理器中需要插入的內存屏障的數量和種類也不同。JMM屏蔽了不同處理器內存模型的差異,它在不同的處理器平臺之上爲Java程序員呈現了一個一致的內存模型。

Q63:Java程序內存可見性保證的分類?
答:①單線程程序:單線程程序不會出現內存可見性問題。編譯器、runtime和處理器會共同確保單線程程序的執行結果與該程序在一致性模型中的執行結果相同。
②正確同步的多線程程序:正確同步的多線程程序的執行將具有順序一致性(程序的執行結果與該程序在一致性模型中的執行結果相同)。這是JMM關注的重點,JMM通過限制編譯器和處理器的重排序來爲程序員提供內存可見性保證。
③未同步/未正確同步的多線程程序:JMM爲它們提供了最小安全性保證,線程執行讀取到的值要麼是之前某個線程寫入的值,要麼是默認值,但不保證該值是正確的。

Q64:JSR-133對舊內存模型的修補有什麼?
答:①增強volatile的內存語義,舊內存模型允許volatile變量與普通變量重排序。JSR-133嚴格限制volatile變量與普通變量的重排序,使volatile的寫-讀和鎖的釋放-獲取具有相同的內存語義。
②增強final的內存語義,舊內存模型中多次讀取同一個final變量的值可能會不相同,爲此JSR-133爲final增加了兩個重排序規則。在保證final引用不會從構造方法逸出的情況下,final具有了初始化安全性。

Q64:什麼是線程?
答:現代操作系統在運行一個程序時會爲其創建一個進程,而操作系統調度的最小單位是線程,線程也叫輕量級進程。在一個進程中可以創建多個線程,這些線程都擁有各自的計數器、堆棧和局部變量等屬性,並且能夠訪問共享的內存變量。處理器在這些線程上告訴切換,讓使用者感覺到這些線程在同時執行。

Q65:爲什麼要使用多線程?
答:①可以更好地利用多處理器核心。線程是大多數操作系統調度的基本單位,一個程序作爲一個進程來運行,程序運行過程中能創建多個線程,而一個線程一個時刻只能運行在一個處理器核心上。單線程最多使用一個處理器核心,加入再多的處理器核心也無法提升程序的執行效率,多線程技術將計算邏輯分配到多個處理器核心,顯著提升程序執行效率。
②可以獲得更快的響應時間。在一些複雜的業務邏輯中,可以使用多線程技術,將數據一致性不強的操作派發給其他線程處理,可以縮短響應時間,提升用戶體驗。
③Java爲程序員提供了良好的一致的編程模型,使開發者可以更加專注於問題的解決,而不是思考如何使其多線程化。

Q66:什麼是線程優先級?
答:現代操作系統基本採用時分形式調度運行的線程,操作系統會分出一個個時間片,線程會分配到若干時間片,當線程的時間片用完了就會發生線程調度,並等待下次分配。線程分配到的時間片多少也就決定了線程使用處理器資源的多少,而線程優先級就是決定線程需要多或者少分配一些處理器資源的線程屬性。

Q67:如何設置線程優先級?
答:在Java中通過一個整形成員變量priority來控制線程優先級,優先級的範圍從1~10,在線程構建的時候可以通過setPriority(int)方法來修改優先級,默認優先級是5,優先級高的線程分配時間片的數量要多於優先級低的線程。
設置線程優先級時,針對頻繁阻塞(休眠或者IO操作)的線程需要設置較高優先級,而偏重計算(需要較多CPU時間或者偏運算)的線程則設置較低的優先級,確保處理器不會被獨佔。在不同的JVM以及操作系統上,線程規劃會存在差異,有些操作系統甚至會忽略對線程優先級的設定。

Q68:線程有哪些狀態?
答:①NEW:初始狀態,線程被構建,但還沒有調用start()方法。②RUNNABLE:運行狀態,Java線程將操作系統中的就緒和運行兩種狀態統稱爲運行中。③BLOCKED:阻塞狀態,表示線程阻塞於鎖。④WAITING:等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程需要等待其他線程做出一些特定動作(通知或中斷)。⑤TIME_WAITING:超時等待狀態,該狀態不同於WAITING,可以在指定時間內自行返回。⑥TERMINATED:終止狀態,表示當前線程已經執行完畢。

Q69:什麼是daemon線程?
答:daemon線程是一種支持型線程,因爲它主要被用作程序中後臺調度以及支持性工作,這意味着當一個Java虛擬機中不存在非daemon線程的時候,Java虛擬機將會退出,可以通過Thread.setDaemon(true)將線程設置爲daemon線程(需要在線程啓動之前設置)。
daemon線程被用於完成支持性工作,但是在JVM退出時daemon線程中的finally塊並不一定會被執行,因爲當JVM中已經沒有非daemon線程時JVM需要立即退出,所有daemon線程都需要立即終止。因此不能依靠finally塊中的內容來確保執行關閉或清理資源的邏輯。

Q70:線程的中斷是什麼?
答:中斷可以理解爲線程的一個標識位屬性,它表示一個運行中的線程是否被其他線程進行了中斷操作。其他線程通過調用該線程的interrupt()方法對其進行中斷操作。
線程通過檢查自身是否被中斷來進行響應,線程通過方法isInterrupted()來判斷是否被中斷,也可以調用靜態方法Thread.interrupted()對當前線程的中斷標識位進行復位。如果該線程處於終結狀態,即使該線程被中毒過,在調用該對象的isInterrupted()時依然返回false。
許多聲明拋出InterruptedException的方法(例如Thread.sleep(long mills))在拋出異常之前,JVM會將該線程的中斷標識位清除,然後再拋出異常,此時調用isInterrupted()時將會返回false。

Q71:爲什麼suspend、resume、stop方法被廢棄了?
答:以suspend方法爲例,在調用後線程不會釋放已經佔有的資源(比如鎖),而是佔着資源進入睡眠狀態,這樣容易引發死鎖問題。同樣,stop方法在終結一個線程時不會保證線程的資源正常釋放,通常是沒有基於線程完成資源釋放工作的機會,因此會導致程序可能運行在不確定狀態下。因爲這些方法的副作用因而被標註爲不建議使用的廢棄方法,而暫停/恢復機制可以用等待/喚醒機制代替。

Q72:可見性問題的原因?
答:Java支持多線程同時訪問一個對象或者對象的成員變量,由於每個線程可以擁有這個變量的拷貝(雖然對象以及成員變量分配的內存是在共享內存中的,但是每個執行的線程還是可以擁有一份拷貝,這樣做的目的是加速程序的執行,這是現代多核處理器的一個顯著特性),所以程序在執行過程中一個線程看到的變量並不一定是最新的。

Q73:volatile和synchronized是如何解決可見性問題的?
答:volatile可以用來修飾成員變量,就是告知程序任何對該變量的訪問均需從共享內存中獲取,而對它的改變必須同步刷新回共享內存,它能保證所有線程對變量訪問的可見性。
synchronized可以修飾方法或者以同步代碼塊的形式進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法和同步代碼塊中,它保證了線程對變量訪問的可見性和排他性。

Q74:監視器是什麼,有什麼作用?
答:任意一個對象都擁有自己的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入同步塊或者同步方法,而沒有獲取到監視器(執行該方法)的線程就會被阻塞在同步塊和同步方法的入口處,進入同步隊列,線程狀態變爲BLOCKED狀態,直到前一個獲得了鎖的線程釋放了鎖該阻塞線程纔會被喚醒重新嘗試獲取監視器。

Q75:wait()、notify()和notifyAll()的使用細節?
答:①使用wait()、notify()和notifyAll()時需要先對調用對象加鎖。②調用wait()方法後,線程狀態由RUNNING變爲WAITING,並將當前線程放置到對象的等待隊列。③notify()和notifyAll()方法調用後,等待線程依舊不會從wait()返回,需要調用notify()和notifyAll()的線程釋放鎖後,等待線程纔有機會從wait()返回。④notify()方法將等待隊列的一個等待線程從等待隊列移到同步隊列中,而notifyAll()方法將等待隊列中的全部線程移到同步隊列,被移動的線程狀態由WAITING變爲BLOCKED。⑤從wait()方法返回的前提是獲得了調用對象的鎖。

Q76:管道IO流的作用?
答:管道IO流和普通IO流或網絡IO流的不同之處在於它主要用於線程之間的數據傳輸,而傳輸的媒介爲內存。管道IO流主要包括4種具體實現:PipedOutputStream、PipedInputStream、PipedWriter、PipedReader,前兩種面向字節,後兩種面向字符。

Q77:join方法的作用?
答:join方法使當前線程必須等待調用join方法的線程執行完畢後才能繼續執行,除了無參join方法外還有帶超時參數的join方法,在指定時間內沒有結束就會從該方法返回。底層是通過wait和notifyAll方法實現的,當調用join的線程終止時會調用自身notifyAll方法通知所有等待在該線程對象上的線程。

Q78:ThreadLocal的作用?
答:ThreadLocal即線程變量,是一個以ThreadLocal對象爲鍵,任意對象爲值得存儲結構。這個結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。可以通過set方法設置一個值,在當線程下再通過get方法獲取到原先設置的值。

Q79:爲什麼要使用線程池?
答:對於服務端的程序經常面對的是客戶端傳入的短小(執行時間短、工作內容較爲單一)任務,需要服務端快速處理並返回結果。如果服務端每接收到一個任務就創建一個線程然後進行執行,這在原型階段是個不錯的選擇但是面對成千上萬的任務遞交服務器時如果還是採用該方式那麼將會創建數以萬計的線程,使操作系統頻繁進行線程上下文切換,無故增加系統負載,而線程的創建和消亡都是需要耗費系統資源的。
線程池技術可以很好地解決該問題,它預先創建了若干數量的線程,並且不能由用戶直接對線程的創建進行控制,在這個前提下重複使用固定或較爲固定數目的線程來完成任務的執行。這樣做一方面消除了頻繁創建和消亡消除的系統資源開銷,另一方面,面對過量任務的提交能夠平緩地處理。

Q80:Lock鎖和synchronized的區別?
答:在JDK1.5之後,併發包中新增了Lock接口以及相關實現類用來實現鎖功能,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了隱式獲取鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字不具備的同步特性。
使用synchronized關鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放,簡化了同步的管理,但是擴展性沒有顯式鎖好。

Q81:Lock接口提供的synchronized關鍵字不具備的主要特性?
答:①Lock可以嘗試非阻塞地獲取鎖,當前線程嘗試獲取鎖,如果這一時刻沒有被其他線程獲取到則成功獲取並持有鎖。②能被中斷地獲取鎖,與synchronized不同,獲取到鎖的線程能夠響應中斷,當獲取到鎖的線程被中斷時,中斷異常將會被拋出,同時鎖會被釋放。③超時獲取鎖,在指定的截至時間之前獲取鎖,如果截止時間到了仍舊無法獲取鎖則返回。
Q82:AQS是什麼?
答:AQS是抽象隊列同步器Abstract Queued Synchronizer,是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作,併發包的作者期望它成爲實現大部分同步需求的基礎。

Q83:AQS的主要實現方式是什麼?
同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的3個方法(getState、setState和compareAndSetState)來進行操作,因爲它們能夠保證狀態的改變是安全的。子類推薦被定義爲自定義同步組件的靜態內部類,同步器自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既可以支持獨佔式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型地同步組件(ReentrantLock、ReentrantReadWriteLock和CountDownLacth等)。

Q84:同步器和鎖的聯繫?
答:同步器是實現鎖的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。鎖是面向使用者的,它定義了使用者與鎖交互的接口,隱藏了實現細節;同步器面對的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所關注的領域。

Q85:AQS的實現包括哪些方面?
答:隊列同步器的從實現角度分爲多方面,主要包括同步隊列、獨佔式同步狀態的獲取與釋放、共享式同步狀態的獲取與釋放,以及超時獲取同步狀態等同步器的核心數據與模板方法。

Q86:同步隊列的原理?
答:同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等構造成一個節點並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。

Q87:同步隊列的節點保存哪些信息?
答:同步隊列中的節點用來保存獲取同步狀態失敗的線程引用、等待狀態以及前驅和後繼節點。節點是構成同步隊列的基礎,同步器擁有首節點和尾節點,沒有成功獲取同步狀態的線程將會成爲節點加入該隊列的尾部。

Q88:同步隊列節點的等待狀態有哪些類型?
答:①CANCELLED,值爲1,由於在同步隊列中等待的線程等待超時或者被中斷需要從同步隊列中取消等待,節點進入該狀態將不會變化。②SIGNAL,值爲-1,後繼節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或者被取消,將會通知後繼節點,使後繼節點的線程得以運行。③CONDITION,值爲-2,節點在等待隊列中,節點線程等待在Condition上,當其他線程對Condition調用了signal方法後該節點將會從等待隊列轉移到同步隊列中,加入到對同步狀態的獲取中。④PROPAGATE,值爲-3,表示下一次共享式同步狀態獲取將會無條件地被傳播下去。⑤INITIAL,值爲0,初始狀態。

Q89:獨佔式同步狀態的獲取和釋放流程?
答:在獲取同步狀態時,同步器調用acquire方法,維護一個同步隊列,使用tryAcquire方法安全地獲取線程同步狀態,獲取狀態失敗的線程會構造同步節點並通過addWaiter方法被加入到同步隊列的尾部,並在隊列中進行自旋。之後會調用acquireQueued方法使得該節點以死循環的方式獲取同步狀態,如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞節點被中斷實現,移出隊列或停止自旋的條件是前驅節點是頭結點並且成功獲取了同步狀態。
在釋放同步狀態時,同步器調用tryRelease方法釋放同步狀態,然後調用unparkSuccessor方法(該方法使用LockSupport喚醒處於等待狀態的線程)喚醒頭節點的後繼節點,進而使後繼節點重新嘗試獲取同步狀態。

Q90:爲什麼只有當前驅節點是頭節點時才能夠嘗試獲取同步狀態?
答:①頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放同步狀態之後,將會喚醒其後繼節點,後繼節點的線程被喚醒後需要檢查自己的前驅節點是否是頭節點。
②維護同步隊列的FIFO原則,節點和節點在循環檢查的過程中基本不相互通信,而是簡單地判斷自己的前驅是否爲頭節點,這樣就使得節點的釋放規則符合FIFO,並且也便於對過早通知的處理(過早通知是指前驅節點不是頭結點的線程由於中斷而被喚醒)。

Q91:共享式同步狀態的獲取和釋放流程?
答:在獲取同步狀態時,同步器調用acquireShared方法,該方法調用tryAcquireShared方法嘗試獲取同步狀態,返回值爲int類型,當返回值大於等於0時,表示能夠獲取到同步狀態。因此在共享式獲取鎖的自旋過程中,成功獲取到同步狀態並退出自旋的條件就是該方法的返回值大於等於0。
釋放同步狀態時,調用releaseShared方法,釋放同步狀態之後將會喚醒後續處於等待狀態的節點。對於能夠支持多線程同時訪問的併發組件,它和獨佔式的主要區別在於tryReleaseShared方法必須確保同步狀態(或資源數)線程安全釋放,一般通過循環和CAS來保證,因爲釋放同步狀態的操作會同時來自多個線程。

Q92:獨佔式超時獲取同步狀態的流程?
答:通過調用同步器的doAcquireNanos方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則返回false。該方法提供了傳統Java同步操作(例如synchronized關鍵字)所不具備的特性。

Q93:響應中斷的同步狀態獲取過程?
答:在JDK1.5之前當一個線程獲取不到鎖而被阻塞到synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標誌位會被修改,但線程依舊阻塞在synchronized上等待着獲取鎖。在JDK1.5中,同步器提供了acquireInterruptibly方法,這個方法在等待獲取同步狀態時,如果當前線程被中斷,會立即返回並拋出InterruptedException。

Q94:獨佔式超時獲取同步狀態的原理?
答:超時獲取同步狀態的過程可以被視爲響應中斷獲取同步狀態過程的“增強版”,doAcquireNanos方法在支持響應中斷的基礎上增加了超時獲取的特性,針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,爲了防止過早通知,nanosTimeout的計算公式爲nanosTimeout-=now-lastTime,其中now爲當前喚醒時間,lastTime爲上次喚醒時間,如果nanosTimeout大於0則表示超時時間未到,需要繼續睡眠nanosTimeout納秒,否則表示已經超時。

Q95:獨佔式超時獲取同步狀態和獨佔式獲取同步狀態的區別?
答:在獨佔式超時獲取同步狀態的過程的doAcquireNanos中,當節點的前驅節點爲頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,這個過程和獨佔式同步獲取的過程類似,但是在同步狀態獲取失敗的處理上有所不同。
如果當前線程獲取同步狀態失敗,獨佔式超時獲取同步狀態中會判斷是否超時,如果沒有超時就重新計算超時間隔,然後使當前線程等待該間隔時間,如果在該時間內沒有獲取到同步狀態就會從等待邏輯中自動返回。而獨佔式獲取同步狀態的過程中如果沒有獲取到同步狀態就會使當前線程一直處於等待狀態。

Q96:超時時間過小時對超時等待有哪些影響?
答:nanosTimeout過小時(小於等於1000納秒),將不會使線程進行超時等待,而是進入快速自旋過程。因爲非常短的超市等待無法做到精確,如果這時再進行超時等待相反會讓nanosTimeout的超時從整體上表現得反而不精確,因此在超市非常短的情況下同步器會進入無條件的快速自旋。

Q97:什麼是可重入鎖?
答:重入鎖就是支持重進入的鎖,它表示該鎖能夠支持一個線程對資源的重複加鎖,除此之外該鎖還支持獲取鎖的公平和非公平性選擇。synchronized關鍵字隱式地支持重進入,ReentrantLock雖然不能像synchronized關鍵字一樣支持隱式的重進入,但是在調用lock方法時已經獲取到鎖的線程能夠再次調用lock方法獲取鎖而不被阻塞。

Q98:什麼是鎖的公平性?
答:如果在絕對時間上,先對鎖進行獲取的請求一定先被滿足,那麼這個鎖是公平的,反正就是不公平的。公平的獲取鎖也就是等待時間最長的線程優先獲取鎖,也可以說鎖的獲取是順序的,ReentrantLock的構造方法中可以通過設置參數控制鎖的公平性。
公平鎖機制往往沒有非公平鎖的效率高,非公平鎖地吞吐量更大,但是公平鎖能夠減少飢餓發生的概率,保證了鎖地獲取按照FIFO順序,等待越久的請求越是能優先得到滿足。

Q99:什麼是重進入?
答:重進入指的是任意線程在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞,該特性的實現需要解決兩個問題:①線程再次獲取鎖,鎖需要去識別獲取鎖的線程是否爲當前佔有鎖的線程,如果是則再次獲取成功。②鎖的最終釋放,線程重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他現場能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而被鎖釋放時,技術自減,當計數爲0時表示鎖已經成功釋放。

Q100:ReentrantLock的可重入如何實現?
答:以非公平鎖爲例,通過nonfairTryAcquire方法獲取鎖,該方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前線程是否爲獲取鎖的線程來決定獲取操作是否成功,如果是獲取鎖的線程再次請求則將同步狀態值進行增加並返回true,表示獲取同步狀態成功。
成功獲取鎖的線程再次獲取鎖,只是增加了同步狀態值,這就要求ReentrantLock在釋放同步狀態時減少同步狀態值。如果該鎖被獲取了n次,那麼前(n-1)次tryRelease方法必須都返回fasle,只有同步狀態完全釋放了才能返回true,可以看到該方法將同步狀態是否爲0作爲最終釋放的條件,當同步狀態爲0時,將佔有線程設置爲null,並返回true,表示釋放成功。

Q101:ReentrantLock的可重入的公平鎖如何實現?
答:對於非公平鎖只要CAS設置同步狀態成功則表示當前線程獲取了鎖,而公平鎖則不同。公平鎖使用tryAcquire方法,該方法與nonfairTryAcquire的唯一區別就是判斷條件中多了對同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之後才能繼續獲取鎖。

Q102:什麼是讀寫鎖?
答:像Mutex和ReentrantLock都是排他鎖,這些鎖在同一時刻只允許一個線程進行訪問,而讀寫鎖在同一時刻可以允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀寫鎖使得併發性相比一般的排他鎖有了很大提升。

Q103:讀寫鎖的特點?
答:除了保證寫操作對讀操作的可見性以及併發性的提升之外,讀寫鎖能夠簡化讀寫交互場景的編程方式。只需要在讀操作時獲取讀鎖,寫操作時獲取寫鎖即可,當寫鎖被獲取時後續(非當前寫操作線程)的讀寫操作都會被阻塞,寫鎖釋放之後所有操作繼續執行,編程方式相對於使用等待/通知機制的實現方式而言變得簡單明瞭。

Q104:讀寫鎖ReentrantReadWriteLock的特性?
答:①公平性選擇:支持非公平(默認)和公平的鎖獲取方式吞吐量還是非公平性優於公平。
②重進入:該鎖支持重進入,以讀寫線程爲例:讀線程在獲取了讀鎖之後能夠再次獲得讀鎖。而寫線程在獲取了寫鎖之後能再次獲得寫鎖,同時也可以獲取讀鎖。
③鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級爲讀鎖。

Q105:讀寫鎖的狀態是怎麼設計的?
答:讀寫鎖同樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。讀寫鎖的自定義同步器需要在同步狀態(一個整形變量)上維護多個讀線程和一個寫線程的狀態。如果在一個int型變量上維護多種狀態,就一定需要“按位切割使用”這個變量,讀寫鎖將變量切分成了兩個部分,高16位表示讀,低16位表示寫。
假設同步狀態值爲S,寫狀態等於S&0x0000FFFF(將高17位全部抹去),讀狀態等於S>>>16(無符號右移16位),當寫狀態增加1時,等於S+1,當讀狀態增加1時,等於S+(1<<16)。根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態等於0時,則讀狀態大於0,即讀鎖已被獲取。

Q106:寫鎖的獲取和釋放過程?
答:寫鎖是一個支持重進入的排他鎖,如果當前線程已經獲得了寫鎖則增加寫狀態,如果當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不爲0)或者該線程不是已經獲得寫鎖的線程則當前線程進入等待狀態。寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態爲0時表示寫鎖已被釋放,從而等待的讀寫線程能夠繼續訪問讀寫鎖,同時前次寫線程的修改對後續讀寫線程可見。

Q107:爲什麼存在讀鎖時寫鎖會阻塞?
答:讀寫鎖要確保寫鎖的操作對讀鎖可見,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那麼正在運行的其他讀線程就無法感知到當前寫線程的操作。因此只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而寫鎖一旦被獲取則其他讀寫線程的後續訪問均被阻塞。

Q108:讀鎖的獲取和釋放過程?
答:讀鎖是一個支持重進入的共享鎖,它能夠被多個線程同時獲取,在沒有其他寫線程訪問(或者寫線程爲0)時,讀鎖總會被成功地獲取,而所做的只是線程安全地增加讀狀態。如果當前線程已經獲取了讀鎖,則增加讀狀態。如果當前線程在獲取讀鎖時,寫鎖已被其他線程獲取則進入等待狀態。如果當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增加讀狀態,成功獲取讀鎖。
讀鎖的每次釋放均會減少讀狀態,減少的值是(1<<16),讀鎖的每次釋放是線程安全的,可能有多個讀線程同時釋放讀鎖。

Q109:JDK1.6對讀鎖有什麼改動?
答:獲取讀鎖的實現從JDK1.5到JDK1.6變得複雜許多,主要原因是新增了一些功能,例如getReadHoldCount方法,作用是返回當前線程獲取讀鎖的次數。讀狀態是所有線程獲取讀鎖次數的總和,而每個線程各自獲取讀鎖的次數只能選擇保存在ThreadLocal中,由線程自身維護,這使獲取讀鎖的實現變得複雜。

Q110:鎖降級是什麼?
答:鎖降級指的是寫鎖降級成爲讀鎖,如果當前線程擁有寫鎖,然後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之爲鎖降級。鎖降級指的是把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放先前擁有的寫鎖的過程。

Q111:鎖降級中讀鎖的獲取是否有必要?
答:是必要的,主要是爲了保證數據的可見性,如果當前線程不獲取讀鎖而是直接釋放寫鎖,假設此刻另一個線程A獲取了寫鎖修改了數據,那麼當前線程是無法感知線程A的數據更新的。如果當前線程獲取讀鎖,即遵循鎖降級的步驟,線程A將會被阻塞,直到當前線程使用數據並釋放讀鎖之後,線程A才能獲取寫鎖並進行數據更新。

Q112:LockSupport是什麼?
答:當需要阻塞或喚醒一個線程的時候,都會使用LockSupport工具類完成相應工作,LockSupport定義了一組公共靜態方法,這些方法提供了最基本的線程阻塞和喚醒功能,而LockSupport也成爲構建同步組件的基礎工具。
LockSupport定義了一組以park開頭的方法用來阻塞當前線程,以及unpark方法來喚醒一個被阻塞的線程。在JDK1.6中,新增了3個含義阻塞對象的park方法,用以替代原有的park方法。

Q113:Condition的作用?
答:Condition接口提供了類似Object監視器方法,與Lock配合可以實現等待/通知模式。Condition對象是由Lock對象創建出來的,因此Condition是依賴Lock對象的。一般會將Condition對象作爲成員變量,當調用await方法後當前線程會釋放鎖並在此等待,而其他線程調用Condition對象的signal方法,通知當前線程後,當前線程才從await方法返回並且在返回前已經獲取了鎖。

Q114:Condition是怎麼實現的?
答:ConditionObject是同步器AQS的內部類,因爲Condition的操作需要獲取相關的鎖,所以作爲同步器的內部類也較爲合理。每個Condition對象都包含着一個等待隊列,該等待隊列是Condition對象實現等待/通知功能的關鍵。Condition的實現主要包括了等待隊列、等待和通知。

Q115:等待隊列的原理?
答:等待隊列是一個FIFO隊列,在隊列中的每個節點都包含了一個線程引用,該線程就是在ConditionObject對象上等待的線程,如果一個線程調用了await方法,那麼該線程會釋放鎖、構造成節點加入等待隊列並進入等待狀態。事實上,節點的定義複用了同步其中節點的定義,也就是說同步隊列和等待隊列中的節點類型都是同步器的靜態內部類Node。
一個ConditionObject包含一個等待隊列,ConditionObject擁有首節點和尾節點。Object擁有首尾節點的引用,而新增節點只需要將原有的尾節點nextWaiter指向它,並且更新尾節點即可。節點引用更新的過程並沒有用CAS保證,因爲調用await方法的線程必定是獲取了鎖的線程,也就是說該過程是由鎖來保證線程安全的。

Q116:await方法的原理?
答:如果從隊列的角度看await方法,當調用await方法時相當於同步隊列的首節點(獲取了鎖的節點)移動到Condition對象的等待隊列中,首節點不會直接加入等待隊列,而是通過addConditionWaiter方法把當前線程構造成一個新的節點並將其加入等待隊列中。加入等待隊列後,釋放同步狀態,喚醒同步隊列中的後繼節點然後進入等待狀態。如果不是通過其他線程調用signal方法喚醒而是對await線程進行中斷,會拋出InterruptedException。

Q117:signal方法的原理?
答:該方法會喚醒在等待隊列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移到同步隊列中。調用該方法的前置條件是當前線程必須獲取了鎖,signal方法進行了檢查,判斷當前線程是否是獲取了鎖的線程,接着獲取等待隊列的首節點,將其移動到同步隊列並使用LockSupport喚醒節點中的線程。被喚醒後的線程將從await方法中的while循環退出,進而調用同步器的acquireQueued方法加入到獲取同步狀態的競爭中。成功獲取同步狀態(或者說鎖)後,被喚醒的線程將從先前調用的await方法返回,此時該線程已成功獲取了鎖。signalAll方法相當於對等待隊列中的每個節點執行一次signal方法,效果就是將等待隊列中的節點全部移到到同步隊列中並喚醒每個節點的線程。

Q118:什麼是阻塞隊列?
答:阻塞隊列是一個支持兩個附加操作的隊列,這兩個附加的操作支持阻塞的插入和移除方法。當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。當隊列爲空時,獲取元素的線程會等待隊列變爲非空。阻塞隊列常用於生產者和消費者的場景,生產者向隊列裏添加元素,消費者從隊列中獲取元素,阻塞隊列就是生產者用來存放元素,消費者用來獲取元素的容器。
Q119:Java中有哪些阻塞隊列?
答:①ArrayBlockingQueue,一個由數組結構組成的有界阻塞隊列,按照FIFO的原則對元素排序,默認情況下不保證線程公平地訪問隊列,有可能先阻塞地線程最後才訪問隊列。
②LinkedBlockingQueue,一個由鏈表結構組成的有界阻塞隊列,隊列的默認和最大長度爲Integer的最大值,按照FIFO原則排序。
③PriorityBlockingQueue,一個支持優先級排序的無界阻塞隊列,默認情況下元素按照順序升序排序。也可以自定義compareTo方法指定元素排序規則,或者初始化時指定構造方法的參數Comparator對元素排序,不能保證同優先級元素的順序。
④DelayQueue,一個支持延時獲取元素的無界阻塞隊列,使用優先級隊列實現。隊列中的元素必須實現Delayed接口,在創建元素時可以指定多久才能從隊列中獲取當前元素,只有延時期滿時才能從隊列中獲取元素。適用於以下場景:①緩存系統的設計,一旦能從延遲隊列獲取元素說明緩存有效期到了。②定時任務調度,保存當天將要執行的任務和執行時間,一旦獲取到任務就立刻開始執行。
⑤SynchronousQueue,一個不存儲元素的阻塞隊列,每一個put操作必須等待一個take操作,否則不能繼續添加元素。默認使用非公平策略,也支持公平策略,適用於傳遞性場景,吞吐量高於ArrayBlockingQueue和LinkedBlockingQueue。
⑥LinkedTransferQueue,一個由鏈表結構組成的無界阻塞隊列,相對於其他阻塞隊列多了tryTransfer和transfer方法。transfe方法:如果當前有消費者正在等待接收元素,transfer方法可以把生產者傳入的元素立刻傳輸給消費者,如果沒有,會將元素放在隊列的尾節點等到該元素被消費者消費了才返回。tryTransfer方法:用來試探生產者傳入的元素能否直接傳給消費者,如果沒有消費者等待接收元素返回false,和transfer的區別時無論消費者是否接受都會立即返回,transfer是等到消費者消費了才返回。
⑦LinkedBlockingDeque,一個由鏈表結構組成的雙向阻塞隊列,可以從隊列的兩端插入和移除元素,多了一個操作隊列的入口,在多線程同時入隊時就少了一半競爭。

Q120:阻塞隊列的實現原理?
答:使用通知模式實現,所謂通知模式就是當生產者往滿的隊列裏添加元素時會阻塞住生產者,當消費者消費了一個隊列中的元素後,會通知生產者當前隊列可用。JDK中使用了Condition條件對象來實現。當往隊列裏插入一個元素,如果隊列不可用,那麼阻塞生產者主要通過LockSupport.park(this)實現。

Q121:原子操作類是什麼?
答:JDK1.5開始提供了atomic包,這個包中的原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式。主要包括4類,原子更新基本類型、原子更新數組、原子更新引用類型和原子更新屬性。在原子更新屬性類中有支持帶有版本號的更新方法,可用於解決CAS操作時出現的ABA問題。

Q122:CountDownLatch的作用?
答:允許一個或多個線程等待其他線程完成操作,構造方法接收一個int類型的參數作爲計數器,如果要等待n個點就傳入n。每次調用countDown方法時n就會減1,await方法會阻塞當前線程直到n變爲0,由於countDown方法可用在任何地方,所以n個點既可以是n個線程也可以是1個線程裏的n個執行步驟。用在多線程時,只需要把這個CountDownLatch的引用傳遞到線程裏即可。

Q123:CyclicBarrier的作用?
答:CyclicBarrier是同步屏障,它的作用是讓一組線程到達一個屏障(或同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被攔截的線程纔會繼續運行。構造方法中的參數表示屏障攔截的線程數量,每個線程調用await方法告訴CyclicBarrier自己已到達屏障,然後當前線程被阻塞。還支持在構造方法中傳入一個Runable類型的任務,當線程到達屏障時會優先執行該任務。適用於多線程計算數據,最後合併計算結果的應用場景。

Q124:CountDownLacth和CyclicBarrier的區別?
答:CountDownLacth的計數器只能用一次,而CyclicBarrier的計數器可使用reset方法重置,所以CyclicBarrier能處理更爲複雜的業務場景,例如計算錯誤時可用重置計數器重新計算。
CyclicBarrier還提供了其他有用的方法,例如getNumberWaiting可以獲取CyclicBarrier阻塞的線程數量,isBroken方法用來了解阻塞的線程是否被中斷。

Q125:Semaphore的作用?
答:Semaphore是信號量,用來控制同時訪問特定資源的線程數量,它通過協調各個線程以保證合理的使用公共資源。信號量可以用於流量控制,特別是公共資源有限的應用場景,比如數據庫連接。Semaphore的構造方法參數接收一個int型數字,表示可用的許可證數量,即最大併發數。使用acquire獲得一個許可證,使用release方法歸還許可證,還可以用tryAcquire嘗試獲得許可證。

Q126:Exchanger的作用?
答:Exchanger交換者是用於線程間協作的工具類,用於進行線程間的數據交換。它提供一個同步點,在這個同步點,兩個線程可以交換彼此的數據。這兩個線程通過exchange方法交換數據,如果第一個線程先執行exchange方法它會一直等待第二個線程也執行exchange方法,當兩個線程都到達同步點時這兩個線程就可以交換數據,將本線程生產出的數據傳遞給對方。應用場景包括遺傳算法、校對工作等。

Q127:線程池有哪些好處?
答:①降低資源消耗,通過重複利用已創建的線程降低線程創建和消耗的開銷。
②提高響應速度,當任務到達時,任務可以不需要等到線程創建就可以立即執行。
③提高線程的可管理性,線程是稀缺資源,如果無限制地創建不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。

Q128:線程池的處理流程?
答:①線程池判斷核心線程池是否已滿,如果不是則創建一個新的工作線程來執行任務(工作線程數<corePoolSize,這一步需要獲取全局鎖)。②如何核心線程池已經滿了,判斷工作隊列是否已滿,如果沒有就將任務存儲在工作隊列中(工作線程數>=corePoolSize)。③如果工作隊列滿了,判斷線程池是否已滿,如果沒有就還是創建一個新的工作線程來執行任務(工作線程數<maximumPoolSize)。④如果線程池已滿,就按照線程池的拒絕執行策略來處理無法執行的任務(工作線程數>maximumPoolSize)。
線程池採取這種設計思路是爲了在執行execute方法時儘可能地避免獲取全局鎖,在線程池完成預熱之後,即當前工作線程數>=corePoolSzie時,幾乎所有的execute方法都是執行步驟2,不需要獲取全局鎖。

Q129:工作線程的任務是什麼?
答:線程池創建線程時,會將線程封裝成工作線程Worker,Worker在執行完任務之後,還會循環獲取工作隊列中的任務來執行。線程池中的線程執行任務分爲兩種情況:①在execute方法中創建一個線程時會讓這個線程執行當前任務。②這個線程執行完任務之後,就會反覆從阻塞工作隊列中獲取任務並執行。

Q130:ThreadPoolExecutor創建有哪些參數,具體含義是什麼?
答:①corePoolSize:線程池的基本大小,當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池的基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads方法,線程池會提前創建並啓動所有的基本線程。
②workQueue:工作隊列,用於保存等待執行任務的阻塞隊列,可以選擇以下的阻塞隊列:ArrayBlockQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockQueue等。
③maximumPoolSize:線程池允許的最大線程數,如果工作隊列已滿,並且創建的線程數小於最大線程數,則線程池還會創建新的線程執行任務,如果使用的時無界阻塞隊列該參數是無意義的。
④threadFactory:用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字。
⑤handler:拒絕策略,當隊列和線程池都滿了說明線程池處於飽和狀態,那麼必須採取一種拒絕策略處理新提交的任務,默認情況下使用AbortPolicy直接拋出異常,CallerRunsPolicy表示重新嘗試提交該任務,DiscardOldestPolicy表示拋棄隊列裏最近的一個任務並執行當前任務,DiscardPolicy表示直接拋棄當前任務不處理。也可以自定義該策略。
⑥keepAliveTime:線程活動的保持時間,線程池工作線程空閒後保持存活的時間,所以如果任務很多,且每個任務的執行時間較短,可以調大時間提高線程的利用率。
⑦unit:線程活動保持時間的單位,有天、小時、分鐘、毫秒、微秒、納秒。

Q131:如何向線程池提交任務?
答:可以使用execute和submit方法向線程池提交任務。execute方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功了。submit方法用於提交需要返回值的任務,線程池會返回一個Future類型的對象,通過該對象可以判斷任務是否執行成功,並且可以通過該對象的get方法獲取返回值,get方法會阻塞當前線程直到任務完成,帶超時參數的get方法會在指定時間內返回,這時任務可能還沒有完成。

Q132:關閉線程池的原理?
答:可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池,它們的原理是遍歷線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,所以無法響應中斷的任務可能永遠無法終止。區別是shutdownNow首先將線程池的狀態設爲STOP,然後嘗試停止所有正在執行或暫停任務的線程,並返回等待執行任務的列表,而shutdown只是將線程池的狀態設爲SHUTDOWN,然後中斷所有沒有正在執行任務的線程。
只要調用了這兩個方法中的一個,isShutdown方法就會返回true,當所有任務都已關閉後才表示線程池關閉成功,這時調用isTerminated方法會返回true。通常調用shutdown方法來關閉線程池,如果任務不一定要執行完則可以調用shutdownNow方法。

Q133:如何合理設置線程池?
答:首先可以從以下角度分析:①任務的性質:CPU密集型任務、IO密集型任務和混合型任務。②任務的優先級:高、中和低。③任務的執行時間:長、中和短。④任務的依賴性:是否以來其他系統資源,如數據庫連接。
性質不同的任務可以用不同規模的線程池分開處理,CPU密集型任務應配置儘可能小的線程,如配置Ncpu+1個線程的線程池。由於IO密集型任務線程並不是一直在執行任務,則應配置儘可能多的線程,如2*Ncpu。混合型任務如果可以拆分將其拆分爲一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大那麼分解後的吞吐量將高於串行執行的吞吐量,如果相差太大則沒必要分解。
優先級不同的任務可以使用優先級隊列PriorityBlockingQueue處理。
執行時間不同的任務可以交給不同規模的線程池處理,或者使用優先級隊列。
以來數據庫連接池的任務,由於線程提交SQL後需要等待數據庫返回的結果,等待的時間越長CPU空閒的時間就越長,因此線程數應該儘可能地設置大一些提高CPU的利用率。
建議使用有界隊列,能增加系統的穩定性和預警能力,可以根據需要設置的稍微大一些。

Q134:線程池如何進行監控?
答:①taskCount,線程池需要執行的任務數量。②completedTaskCount,線程池在運行過程中已經完成的任務數量,小於或等於taskCount。③largestPoolSize,線程池裏曾經創建過的最大線程數量,通過這個數據可以知道線程池是否曾經滿過,如果該數值等於線程池的最大大小表示線程池曾經滿過。④getPoolSize,獲取線程池的線程數量,如果線程池不銷燬的化線程池裏的線程不會自動銷燬,所以這個數值只增不減。⑤getActiveCount,獲取活動的線程數。
通過擴展線程池進行監控,可以繼承線程池來自定義,重寫線程池的beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行後和線程池關閉前來執行一些代碼進行監控,例如監控任務的平均執行時間、最大執行時間和最小執行時間。

Q135:Executor框架的調度模型是什麼?
答:在HotSpot VM的線程模型中,Java線程被一對一映射爲本地操作系統線程,Java線程啓動時會創建一個本地操作系統線程,當該Java線程終止時,這個操作系統線程也會被回收,操作系統會調度所有線程並將它們分配給可用的CPU。
Executor框架的調度模型是一種兩級調度模型。在上層,Java多線程程序通常把應用分解爲若干任務,然後使用用戶級的調度器即Executor框架將這些任務映射爲固定數量的線程;在底層,操作系統內核將這些線程映射到硬件處理器上。

Q136:Executor框架的結構?
答:主要由以下三部分組成:
①任務,包括被執行任務需要實現的接口,Runnable或Callable接口。
②任務的執行,包括任務執行機制的核心接口Executor(Executor框架的基礎,將任務的提交和執行分離開來),以及繼承自Executor的ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。
③異步計算的結果,包括接口Future和實現Future接口的FutureTask類。當我們把Runnable接口或Callable接口的實現類提交(submit)給ThreadPoolExecutor或ScheduledThreadPoolExecutor時,ThreadPoolExecutor或ScheduledThreadPoolExecutor會向我們返回一個FutureTask對象。
Q137:ThreadPoolExecutor是什麼?
答:ThreadPoolExecutor是Executor框架最核心的類,是線程池的實現類,主要有三種。
①FixedThreadPool,可重用固定線程數的線程池,corePoolSize和maximumPoolSize都被設置爲創建時的指定參數nThreads,當線程池中的線程數大於corePoolSize時,keepAliveTime爲多餘的空閒線程等待新任務的最長時間,超過這個時間後多餘的線程將被終止,這裏將其設置爲0L表示多餘空閒線程將被立即終止。該線程池使用的工作隊列是無界阻塞隊列LinkedBlockingQueue(隊列容量爲Integer的最大值)。適用於爲了滿足資源管理的需求,而需要限制當前線程數量的應用場景,適用於負載比較重的服務器。
②SingleThreadExecutor,使用單個線程的線程池,corePoolSize和maximumPoolSize都被設置爲1,其他參數和FiexedThreadPool相同。適用於需要保證順序執行各個任務,並且在任意時間點不會有多個線程是活動的的應用場景。
③CachedThreadPool,一個根據需要創建線程的線程池,corePoolSize被設置爲0,maximumPoolSize被設置爲Integer的最大值,將keepAliveTime設爲60L,意味着空閒線程等待時間最長爲1分鐘。該線程池使用的工作隊列是沒有容量的SynchronousQueue,但是maximumPoolSize設爲Integer最大值,如果主線程提交任務的速度高於線程處理的速度,線程池會不斷創建新線程,極端情況下會創建過多線程而耗盡CPU和內存資源。適用於執行很多短期異步任務的小程序,或者負載較輕的服務器。

Q138:ScheduledThreadPoolExecutor是什麼?
答:ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor,主要用來在給定的延遲之後運行任務,或者定期執行任務。其功能與Timer類似,但是功能更加強大、更靈活。Timer對應的是單個後臺線程,而ScheduledThreadPoolExecutor可以在構造方法中指定多個後臺線程數。爲了實現週期性的執行任務,使用DelayQueue作爲工作隊列,獲取任務和執行週期任務後的處理都不同,主要有兩種。
①ScheduledThreadPool:包含若干線程的ScheduledThreadPoolExecutor,創建固定線程個數的線程池。適用於需要多個後臺線程執行週期任務,同時爲了滿足資源管理的需求而需要限制後臺線程數量的應用場景。
②SingleThreadScheduledExecutor:只包含一個線程的ScheduledThreadPoolExecutor,適用於單個後臺線程執行週期任務,同時需要保證順序執行各個任務的應用場景。

Q139:ScheduledThreadPoolExecutor的原理?
答:將待調度任務放入一個DelayQueue中,調度任務主要有三個參數,long類型的time表示這個任務將要被執行的具體時間,long類型的sequenceNumber表示這個任務被添加到線程池的序號,long類型的period表示任務執行時間間隔。DelayQueue封裝了一個PriorityQueue,隊列按照time進行排序,如果time相同則比較sequenceNumber,越小的排在前面,即如果兩個任務的執行時間相同,先提交的任務先被執行。

Q140:Runnable接口和Callable接口的區別?
答:兩個接口的相同點是Runnable接口和Callable接口的實現類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行,不同點是Runnable不會返回結果,Callable可以返回結構。除了可以自己創建Callable接口的對象外,還可以使用工廠類Executors將一個Runnable對象包裝爲一個Callable對象。

Q141:使用無界阻塞隊列對線程池的影響?
答:①當線程池中的線程數達到corePoolSize之後新任務將在無界隊列中等待,因此線程池中的數量不會超過corePoolSize。②因此使用無界隊列時maximumPoolSize和keepAliveTime均是無效參數。③由於使用無界隊列,線程池不會拒絕任務。

Q142:FutureTask有哪些狀態?
答:FutureTask除了實現了Future接口之外,還實現了Runnable接口。因此FutureTask可以交給Executor執行,也可以由調用線程直接執行即調用FutureTask對象的run方法,根據run方法被執行的時機,FutureTask可以處於三種狀態:①未啓動,當FutureTask對象被創建,且沒有執行run方法之前的狀態。②已啓動,當run方法處於被執行過程中,FutureTask對象處於已啓動狀態。③已完成,當run方法執行後正常完成或執行run方法中拋出異常或調用cancel方法取消時,FutureTask對象處於已完成狀態。
當處於未啓動或已啓動狀態時,get方法將阻塞線程,當處於已完成狀態時會立即返回結果或拋出異常。當處於未啓動狀態時,cancel方法會導致此任務永遠不會執行,當處於已啓動狀態時,執行cancel(true)方法,將以中斷執行此任務的方式來試圖停止該任務,執行cancel(false)方法,將不會對正在執行此任務的線程產生應用,當處於已完成狀態時,cancel方法返回false。

Q143:FutureTask的實現原理?
答:FutureTask的實現基於AQS,基於合成複用的設計原則,FutureTask聲明瞭一個內部私有的繼承於AQS的子類Sync,對Future的所有公有方法的調用都會委託給這個內部的子類。AQS被作爲模板方法模式的基礎類提供給FutureTask的內部子類Sync,這個內部的子類只需要實現狀態檢查和更新的方法即可,這些方法將控制FutureTask的獲取和釋放操作。具體來說,Sync實現了AQS的tryAcquireShared和tryReleaseShared方法來檢查和更新同步狀態。

Q144:基於AQS實現的同步器有什麼共同點?
答:①至少有一個acquire操作,這個操作阻塞調用線程,直到AQS的狀態允許這個線程繼續執行。FutureTask中的acquire操作爲get方法調用。②至少有一個release操作,這個操作改變AQS的狀態,改變後的狀態可允許一個多多個阻塞線程解除阻塞。FutureTask中的release操作包括run方法和cancel方法。

Q145:FutureTask的get方法原理?
答:①調用AQS的acquireSharedInterruptibly方法,首先回調在子類Sync中實現的tryAcquireShared方法來判斷acquire操作是否可以成功。acquire操作成功的條件爲:state爲執行完成狀態或取消狀態,且runner不爲null。②如果成功get方法立即返回,如果失敗則到線程等待隊列中去等待其他線程執行release操作。③當其他線程執行release操作喚醒當前線程後,當前線程再次執行tryAcquireShared將返回1,當前線程將理課線程等待隊列並喚醒它的後繼線程。④返回最終結果或拋出異常。

Q146:FutureTask的run方法原理?
答:①執行在構造方法中的指定任務。②以原子方式更新同步狀態,如果操作成功就設置代表計算結果的變量result的值爲Callable的call方法的返回值,然後調用AQS的releaseShared方法。③AQS的releaseShared方法首先回調子類Sync中實現的tryReleaseShared來執行release操作(設置運行任務的線程runner爲null,然後返回true),然後喚醒線程等待隊列的第一個線程。④調用FutureTask的done方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章