1. sychronied 修飾普通方法和靜態方法的區別, 什麼是可見性
對象鎖是用於對象實例方法, 或者一個對象實例上的, 類鎖是用於類的靜態方法或者一個類的 class
對象上的. 類的對象實例可以有多個, 但是每個類只有一個 class
對象, 所以不同對象實例的對象鎖是互不干擾的, 但是每個類只有一個類鎖. 但是類鎖只是一個概念上的, 並不是真實存在的, 類鎖其實是每個類對應的 class
對象. 類鎖和對象鎖之間是互不干擾的.
可見性是指當多個線程訪問同一個變量時, 一個線程修改了這個變量的值, 其他線程能夠立即看得到修改的值. 由於變成對變量的所有操作都必須在工作內存中進行, 而不能直接讀寫主內存中的變量, 那麼對於共享變量首先是在自己的工作內存, 之後再同步到主內存. 可是並不會及時刷新到主內存中, 而是會存在一定的時間差, 所以這時候線程 A 對共享變量的操作對於線程 B 來說, 就不具備可見性了. 要解決可見性的問題 , 可以使用 volatile
關鍵字或者加鎖.
2. synchronized 的原理以及與 ReentrantLock 的區別.
synchronized
屬於獨佔式悲觀鎖, 是通過 JVM 隱式實現的, synchronized
只允許同一時刻只有一個線程操作資源. 它涉及了兩條指令 monitorenter / monitorexit
, 每個對象都有一個監視器鎖 monitor
, monitor
被佔用時就會處於鎖定狀態, 線程執行 monitorenter
指令時嘗試獲取 monitor
的所有權, 執行 monitorexit
時釋放 monitor
對象. 當其他線程沒有拿到 monitor
對象時, 則需要阻塞等待獲取該對象.
而對於同步方法是依賴了方法修飾符 flags
上的 ACC_SYNCHRONIZED
實現的, JVM 就是根據該標識符來實現方法同步的. 當方法被調用時, 調用指令將會檢查方法的 ACC_SYNCHRONIZED
訪問標誌是否被設置, 如果設置了, 執行線程將先獲取 monitor
, 獲取成功之後才能執行方法體. 執行完後再釋放 monitor
. 在方法執行期間, 其他任何線程無法再獲得同一個 monitor
對象.
ReentrantLock
是 Lock
的默認實現方式之一, 它是基於 AQS 實現的. 通過內部的一個 state
字段來表示鎖是否被佔用. 0 表示未佔用, 此時線程就可以通過 CAS 操作將 state
改爲 1成功獲取鎖. 而其他線程只能排隊等待獲取資源.
兩者相同點:
- 都是用來協調多線程對共享對象, 變量的訪問
- 都是可重入鎖, 同一線程可以多次獲得同一把鎖
- 都保證了可見性和互斥性.
兩者不同點
-
ReentrantLock
顯示的獲得,釋放鎖,synchronized
隱式的獲得, 釋放鎖. -
ReentrantLock
可相應中斷,synchronized
無法相應中斷. -
ReentrantLock
同時實現了公平鎖. -
ReentrantLock
可以知道有沒有成功獲取鎖. -
ReentrantLock
在發生異常時, 如果沒有在finally
中主動通過unlock()
釋放鎖, 則可能造成死鎖線程. 而synchronized
發生異常時, 會主動釋放鎖.
ReentrantLock
的可以看 從 LockSupport 到 AQS 的簡單學習中有源碼分析
3. synchronized 做了哪些優化
JDK 1.6 引入了自旋鎖, 適應性自旋鎖, 鎖消除, 鎖粗化, 以及鎖的升級等技術來減少鎖操作的開銷.
- 鎖消除, 會通過逃逸分析的方式, 去分析加鎖的代碼是否被一個或者多個線程使用, 或者等待被使用. 如果分析證實, 只有一個線程訪問, 在編譯這個代碼段的時候, 就不會生成
synchronized
關鍵字, 僅僅生代碼對應的機器碼 - 適應性自旋鎖: 簡單來說, 就是線程如果自旋成功了, 則下次自旋的次數會更多, 如果自旋失敗了, 則自旋的次數減少.
- 鎖粗化: 將臨近的代碼塊用同一個鎖合併起來. 消除無意義的鎖獲取和釋放, 可以提高程序運行性能
- 鎖升級: 無鎖狀態 -> 偏向鎖狀態 -> 輕量級鎖狀態 -> 重量級鎖.
4. volatile 原理
通過使用 Lock
前綴的指令將當前處理器緩存行的數據寫回到主內存, 將其他處理器的緩存無效. 需要數據操作的時候需要再次去主內存中讀取.
通過插入內存屏障指令來禁止會影響變量可見性的指令重排序.指令如下
- 在每個
volatile
寫操作的前面插入一個StoreStore
屏障. - 在每個
volatile
寫操作的後面插入一個StoreLoad
屏障. - 在每個
volatile
讀操作的後面插入一個LoadLoad
屏障. - 在每個
volatile
讀操作的後面插入一個LoadStore
屏障.
詳情見:從 java 內存模型到 volatile 的簡單理解
5. volatile 和 synchronize 有什麼區別?
volatile
是最輕量的同步機制, 它保證了不同線程對這個變量進行操作時的可見性, 即一個線程修改了某個變量的值, 這個值對其他線程來說是立即可見的. 但是無法保證操作的原子性, 因此多線程下寫的複合操作會導致線程安全問題.
synchronize
可以修飾方法或者以同步塊的形式來進行使用, 確保了多個線程在同一時刻只能有一個線程處於方法或者同步塊中, 保證了線程對變量訪問的可見性和排他性, 又稱爲內置鎖機制.
6. CAS 無鎖編程的原理
線程處理器基本都支持 CAS 指令, 只不過實現的算法不同, 每一個 CAS 操作都分爲三個運算符, 一個內存地址 V, 一個期望值 A 和一個新值 B. 操作的時候如果這個地址上存放的值等於期望值 A, 則將地址上的值更新爲 B. 否則不做任何操作.
CAS 的基本思路就是: 如果這個地址上的值和期望值相等, 則給其賦予新值, 否則不做任何事. 但是要返回原值是多少. 循環 CAS 就是在一個循環裏不斷的做 CAS 操作, 直到成功爲止.
使用 CAS 同時帶來了三大問題
ABA 問題: 簡單來說就是說一個原值是 A , 有一個線程將其變成了 B 接着又變成了 A, 那麼使用 CAS 進行比較檢查的時候發現值是沒有任何變化的. 但是實際上也是發生了變化. 解決方法是使用版本號的方式來解決. , 就是在變量前追加上版本號. 每次變量更新的時候把版本號加 1, 那麼 A-B-A, 就變成了 1A-2B-3A. JDK 也同樣提供了兩個類來幫助我們實現這個版本號的問題, 分別是
AtomicStampedReference
,AtomicMarkableReference
.循環時間久開銷大: 自旋 CAS 如果長時間都不成功, 那麼會給 CPU 帶來非常大的執行開銷. 如果 JVM 能支持處理器提供的
pause
指令, 那麼效率會有一定的提升.pause
指令有兩個作用, 第一可以延遲流水線執行指令, 使 CPU 不會消耗過多的執行資源. 第二可以避免在退出循環的時候因內存順序衝突而引起的 CPU 流水線被清空. 從而提高 CPU 執行效率.只能保證一個共享變量的原值操作: 當對一個共享變量進行操作時, 可以慫恿自旋 CAS 的方式來保證原子性, 但是對於多個共享變量操作時, 自旋 CAS 就無法保證其原子性, 這時候就可以使用鎖機制, 也可使用 JDK 提供的
AtomicReference
原子操作類來保證引用對象之間的原子性, 就可以將多個原子變量放到一個對象裏進行 CAS 操作.
詳情見:java 基礎回顧 - 基於 CAS 實現原子操作的基本理解
7. AQS 原理
AQS 即抽象的隊列同步器. 是用來構建鎖或者其他同步組件的基礎框架. 不能被實例化, 設計之初就是爲了讓子類通過繼承 AQS 並實現它的抽象方法來管理同步狀態. 如 ReentrantLock, ReentrantReadWriteLock, CountDownLatch
就是基於 AQS 實現的.
AQS 是基於 CLH 隊列的變體實現的, 是一個雙向同步隊列. 它獲取不到共享資源的線程封裝成爲一個 Node
節點加入到隊列中, 每個 Node
節點維護一個 prev
與 next
引用. 分別指向自己的前驅節點與後置節點. 通過 CAS, 自旋, 以及 LockSuppor.park()
等方式維護內部的一個使用 volatile
修飾的 int
類型共享變量 state
的狀態, 使併發達到同步的效果.
詳情見: 從 LockSupport 到 AQS 的簡單學習
8. 線程的聲明週期
Java 中線程的狀態分爲 6 種.
- 初始狀態: 新建了一個線程對象, 但是還未調用
start
方法. - 運行狀態: Java 線程中獎就緒
(ready)
和運行(running)
兩種狀態籠統的成爲 運行狀態.- 線程對象創建後, 其他線程
(比如 main 線程)
調用了該對象的start()
方法, 該狀態的線程位於可運行線程池中, 等待被線程調度選中, 從而獲得 CPU 的使用權, 此時就處於就緒狀態(ready)
. - 就緒狀態的線程在獲得CPU 時間片後變爲運行中狀態
(running)
.
- 線程對象創建後, 其他線程
- 阻塞狀態.
- 等待狀態: 進入該狀態的線程需要等待其他線程做出一些特定動作
(通知或中斷)
- 超時等待: 該狀態不同與等待狀態, 它可以在指定的時間後執行返回.
- 終止狀態: 表示該線程已經執行完畢.
詳情見: java 基礎回顧 - 線程基礎
9. sleep, wait, yield 的區別, wait 的線程如何喚醒
sleep, yield
被調用後, 都不會釋放當前線程所持有的鎖.
調用 wait
方法會釋放當前線程持有的鎖, 而且被喚醒後會重新去競爭鎖, 鎖競爭到後纔會執行 wait
方法後面的代碼.
wait
通常被通用語線程間交互.
sleep
通常被用於暫停執行.
yield
方法使當前線程讓出 CPU 執行權.
調用wait
方法後使用 notify / notifyAll
進行喚醒.
9. ThreadLocal 是什麼
ThreadLocal
爲每個線程都提供了變量的副本, 是的每個線程在某一時間訪問到的並非同一對象, 這樣就隔離了多個線程對數據的數據共享. 在內部實現上, 每個線程內部都有一個 ThreadLocalMap
, 用來保存每個線程所擁有的變量副本.
詳情可見: Android 消息機制之 ThreadLocal 深入源碼分析 [ 二 ]
10. 爲什麼要使用線程池
合理的使用線程池能夠帶來下面三個好處.
- 降低資源消耗. 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗.
- 可以控制最大併發數. 避免大量線程之間因相互搶佔系統資源而導致的阻塞現象.
- 提高線程的可管理性. 線程是稀缺資源, 如果無限制的創建, 不僅會消耗系統資源, 還會降低系統的穩定性. 使用線程池可以進行統一分配, 調優和監控.
線程池執行任務的流程
- 如果當前運行的線程小於
corePoolSize
, 則創建新線程來執行任務. - 如果運行的線程等於或大於
corePoolSize
, 則將新任務加入到BlockingQueue
. - 如果無法加入
BlockingQueue
(隊列已滿), 則創建新的線程來處理任務. - 如果創建新線程使當前運行的線程超出了
maximumPoolSize
, 那麼執行拒絕策略.
線程池中的幾種拒絕策略爲一下幾種
-
AbortPolicy
: 直接拋出異常. 線程池中默認的拒絕策略. -
DiscardOldestPolicy
: 直接丟棄阻塞隊列中最老的任務, 也就是最前面的任務, 並執行當前任務. -
CallerRunsPolicy
: 提交任務所在的線程來執行這個要提交的任務. -
DiscardPolicy
: 直接丟棄最新的任務, 也就是最後面的.
也可以根據場景來實現RejectedExecutionHandler
接口, 自定義拒絕策略, 比如記錄日誌或者持久化存儲被拒絕的任務.
詳情見: 重識 java 線程池