synchronized關鍵字的底層原理?
用於線程同步,加鎖。
可用於類,對象,塊。一般是對一個對象進行加鎖。
// 線程1 synchronized(myObject) { -> 類的class對象來走的 // 一大堆的代碼 synchronized(myObject) { // 一大堆的代碼 } }
synchronize底層原理與JVM指令和monitor有關系。深入涉及CPU硬件原理,原則性、可見性、有序性、指令重排、偏向鎖、JDK的對其進行的優化。
synchronized關鍵字,底層編譯后的JVM指令中,使用monitorenter和monitorexit指令。
monitorenter:加鎖
monitorexit:解鎖
如果一個線程第一次synchronized那里,獲取到了myObject對象的monitor的鎖,計數器加1,然后第二次synchronized那里,會再次獲取myObject對象的monitor的鎖,這個就是重入加鎖了,然后計數器會再次加1,變成2。
這個時候,其他的線程在第一次synchronized那里,會發現說myObject對象的monitor鎖的計數器是大於0的,意味着被別人加鎖了,然后此時線程就會進入block阻塞狀態,什么都干不了,就是等着獲取鎖。
接着如果出了synchronized修飾的代碼片段的范圍,就會有一個monitorexit的指令,在底層。此時獲取鎖的線程就會對那個對象的monitor的計數器減1,如果有多次重入加鎖就會對應多次減1,直到最后,計數器是0。
聊聊CAS的理解以及其底層實現原理?
CAS: Compare And Set
CAS在底層的硬件級別保證原子性,同一時間只有一個線程可以執行CAS,先比較再設置,其他線程的CAS同時執行此時會失敗。
ConcurrentHashMap實現線程安全的底層原理是什么?
JDK1.8之前,多個數組,分段加鎖,一個數組一個鎖。
JDK1.8之后,優化細粒度,一個數組,每個元素進行CAS,如果失敗,則有線程已經用synchronized對元素加鎖。
鏈表+紅黑樹處理,對數組每個元素加鎖。
多個線程要訪問同一個數據,synchronized加鎖,CAS去進行安全的累加,去實現多線程場景下的安全的更新一個數據的效果。
JDK1.8 [一個大的數組],數組里每個元素進行put操作,都是有一個不同的鎖,剛開始進行put的時候,如果兩個線程都是在數組[5]這個位置進行put,這個時候,對數組[5]這個位置進行put的時候,采取的是CAS的策略,同一個時間,只有一個線程能成功執行這個CAS,就是說他剛開始先獲取一下數組[5]這個位置的值,是null,然后執行CAS,線程1,比較一下,put進去我的這條數據,同時間,其他的線程執行CAS,都會失敗。
通過對數組每個元素執行CAS的策略,如果是很多線程對數組里不同的元素執行put,大家是沒有關系的,可以並行。
如果其他線程失敗了,其他線程此時會發現數組[5]這位置,已經給剛才有線程放進去值了,就需要在這個位置基於鏈表+紅黑樹來進行處理,synchronized(數組[5]),加鎖,基於鏈表或者是紅黑樹在這個位置插進去自己的數據,如果你是對數組里同一個位置的元素進行操作,才會加鎖串行化處理;如果是對數組不同位置的元素操作,此時大家可以並發執行的。
你對JDK中的AQS的理解?AQS的實現原理?
ReentrantLock
state變量 -> CAS -> 失敗后進入隊列等待 -> 釋放鎖之后喚醒
非公平鎖 公平鎖
AQS=Abstract Queue Synchronizer 抽象隊列同步器
線程池連環炮:
說說線程池的底層工作原理?說說線程池核心配置參數?線程池隊列滿了之后會發生什么事情?
如果在線程中使用無界阻塞隊列會發生什么問題?如果宕機,線程池的阻塞隊列中的請求怎么辦?
1. 線程池底層工作原理?
固定數量線程池:
ExecutorService threadPool = Executors.newFixedThreadPool(3) -> 3: corePoolSize threadPool.submit(new Callable() { public void run() {} });
提交任務,先看一下線程池里的線程數量是否小於corePoolSize,也就是3,如果小於,直接創建一個線程出來執行你的任務。
如果執行完你的任務之后,這個線程是不會死掉的,他會嘗試從一個無界的LinkedBlockingQueue里獲取新的任務,如果沒有新的任務,此時就會阻塞住,等待新的任務到來。
你持續提交任務,上述流程反復執行,只要線程池的線程數量小於corePoolSize,都會直接創建新線程來執行這個任務,執行完了就嘗試從無界隊列里獲取任務,直到線程池里有corePoolSize個線程。
接着再次提交任務,會發現線程數量已經跟corePoolSize一樣大了,此時就直接把任務放入隊列中就可以了,線程會爭搶獲取任務執行的,如果所有的線程此時都在執行任務,那么無界隊列里的任務就可能會越來越多。
2. 說說線程池的核心配置參數都是干什么的?平時我們應該怎么用?
線程池類:ThreadPoolExecutor
創建一個線程池就是這樣子的,corePoolSize,maximumPoolSize,keepAliveTime,queue,這幾個東西,如果你不用fixed之類的線程池,自己完全可以通過這個構造函數就創建自己的線程池。
corePoolSize:3
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:60s
new ArrayBlockingQueue<Runnable>(200)
設你的maximumPoolSize是比corePoolSize大的,此時會繼續創建額外的線程放入線程池里,來處理這些任務,然后超過corePoolSize數量的線程如果處理完了一個任務也會嘗試從隊列里去獲取任務來執行。
如果額外線程都創建完了去處理任務,隊列還是滿的,此時還有新的任務來只能拒絕。有幾種reject策略,可以傳入RejectedExecutionHandler接口。可以自己實現這個接口,實現對這些超出數量的任務的處理
ThreadPoolExecutor已經實現4個拒絕策略:
(1)AbortPolicy:直接拋異常
(2)DiscardPolicy:當前任務會強制調用run先執行,任務將由調用者線程(可能是主線程)去執行。缺點可能會阻塞主線程。
(3)DiscardOldestPolicy:拋棄任務隊列中最舊任務
(4)CallerRunsPolicy:拋棄當前將要加入隊列的任務
(5)自定義
如果后續慢慢的隊列里沒任務了,線程空閑了,超過corePoolSize的線程會自動釋放掉,在keepAliveTime之后就會釋放。
如果線上機器突然宕機,線程池的阻塞隊列中的請求怎么辦?
必然導致線程池里積壓的任務會丟失。
解決方案:
如果你要提交一個任務到線程池里,在插入之前,先在數據庫里插入這個任務信息,增加狀態:未提交、已提交、已完成等。
提交成功之后,更新他的狀態是已提交狀態。
系統重啟后,后期去查詢數據庫里未提交和已提交的任務,把任務信息讀取出來,重新提交到線程池,繼續執行。
參考資料:
互聯網Java工程師面試突擊(第三季)-- 中華石杉