Java併發-不懂原理多吃虧

喝水會長肉 發表於 2021-11-25
Java

一、前言

併發程式設計相比 Java 中其他知識點學習門檻較高,從而導致很多人望而卻步。但無論是職場面試,還是高併發/高流量的系統的實現,卻都離不開併發程式設計,於是能夠真正掌握併發程式設計的人成為了市場迫切需求的人才。

二、學習併發程式設計

Java併發程式設計作為Java技術棧中的一塊頂樑柱,其學習成本還是比較大的,很多人學習起來感到沒有頭緒,感覺無從下手?那麼學習併發程式設計是否有一些技巧在裡面那?

其實為了讓開發者從Java併發程式設計的苦海中解脫出來,大神Doug Lea特意為Java開發人員做了一件事情,那就是在JDK中提供了Java 併發包(JUC),該包提供了常用的併發相關的工具類,比如鎖、併發安全的佇列、併發安全的列表、執行緒池、執行緒同步器等。有了JUC包,開發人員編寫併發程式時候,不在那麼吃力了,但是工具雖好,但是如果你對其原理不瞭解,還是很容易犯錯,也就是不懂原理,多吃虧。

比如最簡單的併發安全的佇列LinkedBlockingQueue,其offer與put方法的區別,什麼時候用offer,什麼時候用put,你可能在某個時間點知道,但是過一段時間你就可能會忘記,但是如果你對其原理了解,翻看下程式碼,就可以知道offer是非阻塞的,佇列滿了了,就丟棄當前元素;put是阻塞的,佇列滿則會掛起當前執行緒進行等待。

比如使用執行緒池時候,意在讓呼叫執行緒把任務放入執行緒池後直接返回,讓任務非同步執行。如果你沒注意如果拒絕策略為CallerRunsPolicy,並且不知道執行緒池佇列滿後,拒絕策略的執行是當前呼叫執行緒,而你在拒絕策略裡面做了很耗時的動作,則當前呼叫執行緒就會被阻塞很久。

比如當你使用Executors.newFixedThreadPool等建立執行緒池時候,如果你不知道其內部是建立了一個無界佇列,那麼當大量任務被投遞到建立的執行緒池裡面後,可能就會造成OOM。另外當你不知道執行緒池裡面的執行緒是使用者執行緒或者是deamon執行緒時候,並且沒有呼叫執行緒池的shutdown方法,則建立執行緒池的應用可能就不能優雅退出。

上面列出了幾個例子,意在說明雖然有了JUC包,其實還有很多例項可以說明,不懂原理,多吃虧。那麼我們為何不能花些時間來研究下JUC包重要元件實現原理那?有人可能會說,我有去看啊,但是看不懂啊?每個元件裡面涉及的知識太多了。沒錯,JUC包的實現確實是併發程式設計基礎知識搭建起來的,所以在看元件原理實現前,大家應該先去把併發相關的基礎學好了,並且由淺入深的進行研究。

比如最基礎的執行緒基礎操作原語原語notify/wait系列,join方法,sleep方法,yeild方法,執行緒中斷的理解,死鎖的產生與避免,什麼時候使用者執行緒與deamon執行緒,什麼是偽共享以及如何解決?Java記憶體模型是什麼?什麼是記憶體不可見性以及如何避免?volatile與Synchronized記憶體語義是什麼,用來解決什麼問題?什麼是CAS操作,其出現為了解決什麼問題,其本身存在什麼問題,ABA問題是什麼?什麼是指令重排序,如何避免?什麼是原子性操作?什麼是獨佔鎖,共享鎖,公平鎖,非公平鎖?

如果你已經掌握了上面基礎,那麼你可以先看JUC包中最簡單的基於CAS無鎖實現的原子性操作類比如AtomicLong的實現,你會疑問其中的變數value為何使用volatile修飾(多執行緒下保證記憶體可見性)?然後大家可以看JDK8新增原子操作類LongAdder,在非常高的併發請求下AtomicLong的效能會受影響,雖然AtomicLong使用CAS但是CAS失敗後還是通過無限迴圈的自旋鎖不斷嘗試的,在高併發下N多執行緒同時去操作一個變數會造成大量執行緒CAS失敗然後處於自旋狀態,這大大浪費了cpu資源,降低了併發性。那麼既然AtomicLong效能由於過多執行緒同時去競爭一個變數的更新而降低的,那麼如果把一個變數分解為多個變數,讓同樣多的執行緒去競爭多個資源那麼效能問題不就解決了?是的,JDK8提供的LongAdder就是這個思路。看到這裡大家或許會眼前一亮,原來如此。然後可以看比較簡單的併發安全的基於寫時拷貝的CopyOnWriteArrayList的實現,以及探究其迭代器的弱一致性的實現原理(也就是寫時拷貝),雖然其實現裡面用到了獨佔鎖,但是可以先不用深入鎖的細節。

如果你已經掌握了上面內容,那麼下面就如核心環節,也就是對JUC包中鎖的研究,一開始你肯定要先把LockSupport類研究透,其是鎖中讓執行緒掛起與喚醒的基礎設施。由於鎖是基於AQS(AbstractQueuedSynchronizer)實現的,所以你肯定要先把AQS搞清楚了,你會發現AQS 中維持了一個單一的狀態資訊 state, 可以通過 getState,setState,compareAndSetState 函式修改其值;對於 ReentrantLock 的實現來說,state 可以用來表示當前執行緒獲取鎖的可重入次數;對應讀寫鎖 ReentrantReadWriteLock 來說 state 的高 16 位表示讀狀態也就是獲取該讀鎖的次數,低 16 位表示獲取到寫鎖的執行緒的可重入次數;對於 semaphore 來說 state 用來表示當前可用訊號的個數;對於 FutuerTask 來說,//java學習交流:737251827  進入可領取學習資源及對十年開發經驗大佬提問,免費解答! state 用來表示任務狀態(例如還沒開始,執行,完成,取消);對應 CountDownlatch 和 CyclicBarrie 來說 state 用來表示計數器當前的值。

你會知道AQS 有個內部類 ConditionObject 是用來結合鎖實現執行緒同步,ConditionObject 可以直接訪問 AQS 物件內部的變數,比如 state 狀態值和 AQS 佇列;ConditionObject 是條件變數,每個條件變數對應著一個條件佇列 (單向連結串列佇列),用來存放呼叫條件變數的 await() 方法後被阻塞的執行緒。

你會知道 AQS 類並沒有提供可用的 tryAcquire 和 tryRelease,正如 AQS 是鎖阻塞和同步器的基礎框架,tryAcquire 和 tryRelease 需要有具體的子類來實現。子類在實現 tryAcquire 和 tryRelease 時候要根據具體場景使用 CAS 演算法嘗試修改狀態值 state, 成功則返回 true, 否者返回 false。子類還需要定義在呼叫 acquire 和 release 方法時候 state 狀態值的增減代表什麼含義。

比如繼承自 AQS 實現的獨佔鎖 ReentrantLock,定義當 status 為 0 的時候標示鎖空閒,為 1 的時候標示鎖已經被佔用,在重寫 tryAcquire 時候,內部需要使用 CAS 演算法看當前 status 是否為 0,如果為 0 則使用 CAS 設定為 1,並設定當前執行緒的持有者為當前執行緒,並返回 true, 如果 CAS 失敗則 返回 false。

比如繼承自 AQS 實現的獨佔鎖實現 tryRelease 時候,內部需要使用 CAS 演算法把當前 status 值從 1 修改為 0,並設定當前鎖的持有者為 null,然後返回 true, 如果 cas 失敗則返回 false。

AQS知道什麼東東了,然後鎖的話肯定是先看最簡單的獨佔鎖ReentrantLock了,你可以先畫出其類圖結構,看看其有哪些變數和方法,你會發現其分公平鎖與獨佔鎖之分(回顧基礎篇?),類圖中狀態值state代表執行緒獲取該鎖的可重入次數,當一個執行緒第一次獲取該鎖時候state的值為0,該執行緒第二次獲取後該鎖狀態值為1,這就是可重入次數。然後加大難度,看看讀寫鎖ReentrantReadWriteLock是怎麼玩的,當然還有JDK新增的StampedLock別忘了。

等鎖研究完了,那麼你可以對併發佇列進行研究了,其中佇列要分基於CAS的無阻塞佇列ConcurrentLinkedQueue 和其他的基於鎖的阻塞佇列,自然先看比較簡單的ArrayBlockingQueue,LinkedBlockingQueue,ConcurrentLinkedQueue,別忘了高階的優先順序佇列PriorityBlockingQueue和延遲佇列DelayQueue了。

不對,是漏了一大塊了,執行緒池那?,執行緒池主要解決兩個問題:一方面當執行大量非同步任務時候執行緒池能夠提供較好的效能,在不使用執行緒池的時候,每當需要執行非同步任務時候是直接 new一執行緒進行執行,而執行緒的建立和銷燬是需要開銷的。使用執行緒池時候,執行緒池裡面的執行緒是可複用的,不會每次執行非同步任務時候都重新建立和銷燬執行緒。另一方面執行緒池提供了一種資源限制和管理的手段,比如可以限制執行緒的個數,動態新增執行緒等,每個 ThreadPoolExecutor 也保留了一些基本的統計資料,比如當前執行緒池完成的任務數目等。

這就完了?不,前面講解過 Java 中執行緒池 ThreadPoolExecutor 原理探究,ThreadPoolExecutor 是 Executors 工具類裡面的一部分功能,下面來介紹另外一部分功能也就是 ScheduledThreadPoolExecutor 的實現,後者是一個可以指定一定延遲時間後或者定時進行任務排程執行的執行緒池。

等等,有實踐?當然要有,雖然Java併發程式設計內容很廣,但是還是有一些規則可以遵循的,比如執行緒,執行緒池建立時候要指定名稱以便排查問題,執行緒池使用完畢記得關閉,ThreadLocal使用完畢記得呼叫remove清理,SimpleDateFormat是執行緒不安全的等等。

如果你對上面內容感興趣,並且對學併發無從下手,那麼機會來了,《Java併發程式設計之美》這本書,就是按照這個思路來編寫的


那麼該書把JUC包寫完了?不,其實非同步執行的Future也是JUC中一特色,特別是CompleteFuture的出現,其與NIO結合,簡直是天合之作。另外CompleteFuture任務預設執行使用的ForkJoinPool框架的commonPool執行緒池,那你應該明白了,這些本書是沒提到的


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70010294/viewspace-2844109/,如需轉載,請註明出處,否則將追究法律責任。