線程方面的總結

1. sychronied 修飾普通方法和靜態方法的區別, 什麼是可見性

對象鎖是用於對象實例方法, 或者一個對象實例上的, 類鎖是用於類的靜態方法或者一個類的 class 對象上的. 類的對象實例可以有多個, 但是每個類只有一個 class 對象, 所以不同對象實例的對象鎖是互不干擾的, 但是每個類只有一個類鎖. 但是類鎖只是一個概念上的, 並不是真實存在的, 類鎖其實是每個類對應的 class 對象. 類鎖和對象鎖之間是互不干擾的.

可見性是指當多個線程訪問同一個變量時, 一個線程修改了這個變量的值, 其他線程能夠立即看得到修改的值. 由於變成對變量的所有操作都必須在工作內存中進行, 而不能直接讀寫主內存中的變量, 那麼對於共享變量首先是在自己的工作內存, 之後再同步到主內存. 可是並不會及時刷新到主內存中, 而是會存在一定的時間差, 所以這時候線程 A 對共享變量的操作對於線程 B 來說, 就不具備可見性了. 要解決可見性的問題 , 可以使用 volatile 關鍵字或者加鎖.

詳情見:從 Synchronized 到鎖的優化
 

2. synchronized 的原理以及與 ReentrantLock 的區別.

synchronized 屬於獨佔式悲觀鎖, 是通過 JVM 隱式實現的, synchronized 只允許同一時刻只有一個線程操作資源. 它涉及了兩條指令 monitorenter / monitorexit, 每個對象都有一個監視器鎖 monitor, monitor 被佔用時就會處於鎖定狀態, 線程執行 monitorenter 指令時嘗試獲取 monitor 的所有權, 執行 monitorexit 時釋放 monitor 對象. 當其他線程沒有拿到 monitor 對象時, 則需要阻塞等待獲取該對象.
而對於同步方法是依賴了方法修飾符 flags 上的 ACC_SYNCHRONIZED 實現的, JVM 就是根據該標識符來實現方法同步的. 當方法被調用時, 調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置, 如果設置了, 執行線程將先獲取 monitor, 獲取成功之後才能執行方法體. 執行完後再釋放 monitor. 在方法執行期間, 其他任何線程無法再獲得同一個 monitor 對象.

ReentrantLockLock 的默認實現方式之一, 它是基於 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. volatilesynchronize 有什麼區別?

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 節點維護一個 prevnext引用. 分別指向自己的前驅節點與後置節點. 通過 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 線程池
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章