Java 并发编程-不懂原理多吃亏(送书福利)
作者 | 加多
關注阿里巴巴云原生公眾號,后臺回復關鍵字“并發(fā)”,即可參與送書抽獎!
**
導讀:并發(fā)編程與 Java 中其他知識點相比較而言學習門檻較高,從而導致很多人望而卻步。但無論是職場面試,還是高并發(fā)/高流量系統(tǒng)的實現(xiàn),都離不開并發(fā)編程,于是能夠真正掌握并發(fā)編程的人成為了市場迫切需求的人才。本文中,作者加多以通俗易懂的方式講解了多線程并發(fā)編程從入門到實踐需要掌握的理論知識與實際操作方法。
學習并發(fā)編程
Java 并發(fā)編程作為 Java 技術(shù)棧中的一根頂梁柱,其學習成本還是比較大的,很多人學習起來感到?jīng)]有頭緒、無從下手。那么學習并發(fā)編程是否有一些技巧在里面呢?
為了讓開發(fā)者從 Java 并發(fā)編程的苦海中解脫出來,大神 Doug Lea 特意為 Java 開發(fā)人員做了一件事情,那就是在 JDK 中提供了 Java 并發(fā)包(JUC)。
該包提供了常用的并發(fā)相關的工具類,比如鎖、并發(fā)安全的隊列、并發(fā)安全的列表、線程池、線程同步器等。有了 JUC 包,開發(fā)人員編寫并發(fā)程序的時候,就不再那么吃力了;但是工具雖好,如果你對其原理不了解,還是很容易犯錯,即:不懂原理多吃虧。
下面為大家舉三個例子進行說明:
- 最簡單的并發(fā)安全隊列 LinkedBlockingQueue,其 offer 與 put 方法的區(qū)別。什么時候用 offer,什么時候用 put,你可能在某個時間點知道,但是過一段時間可能就會忘記。但如果你對其原理了解,翻看下代碼,就可以知道:offer 是非阻塞的,隊列滿了,就丟棄當前元素;put 是阻塞的,隊列滿則會掛起當前線程進行等待;
- 使用線程池的時候,意在讓調(diào)用線程把任務放入線程池后直接返回,讓任務異步執(zhí)行。如果你沒注意拒絕策略為 CallerRunsPolicy,并且不知道線程池隊列滿后,拒絕策略的執(zhí)行是當前調(diào)用線程,那么你在拒絕策略里面就會做很耗時的動作,導致當前調(diào)用線程被阻塞很久;
- 當你使用 Executors.newFixedThreadPool 等創(chuàng)建線程池的時候,如果你不知道其內(nèi)部創(chuàng)建了一個無界隊列,那么當大量任務被投遞到創(chuàng)建的線程池里面后,可能就會造成 OOM(OutOfMemoryError)。另外當你不知道線程池里面的線程是用戶線程還是 deamon 線程的時候,且沒有調(diào)用線程池的 shutdown 方法,則創(chuàng)建線程池的應用也許就不能優(yōu)雅退出。
上面的幾個例子,意在說明雖然有了 JUC 包,但是不懂原理依然會很吃虧。那么我們?yōu)楹尾换ㄐr間來研究下 JUC 包重要組件的實現(xiàn)原理呢?
有人可能會說:我看了但看不懂,每個組件里面涉及的知識太多了。沒錯, JUC 包重要組件的實現(xiàn)的確是由并發(fā)編程基礎知識搭建起來的,所以大家在看組件實現(xiàn)原理前,應該先去把并發(fā)的相關基礎知識學好,然后由淺入深進行研究。
比如最基礎的線程基礎操作原語 notify/wait 系列,join 方法、sleep 方法、yeild 方法;線程中斷的理解;死鎖的產(chǎn)生與避免;什么時候是用戶線程、什么時候是 deamon 線程?什么是偽共享以及如何解決?Java 內(nèi)存模型是什么?什么是內(nèi)存不可見性以及如何避免?volatile 與 Synchronized 內(nèi)存語義是什么,它是用來解決什么問題的?什么是 CAS 操作,它的出現(xiàn)為了解決什么問題?ABA 問題是什么?什么是指令重排序,如何避免?什么是原子性操作?什么是獨占鎖,共享鎖,公平鎖,非公平鎖?······
如果你已經(jīng)掌握了上面列出的所有基礎知識,那么就可以先看 JUC 包中最簡單的基于 CAS 無鎖實現(xiàn)的原子性操作類如:AtomicLong 的實現(xiàn)??赡苣銜兴蓡?#xff1a;其中的變量 value 為何使用 volatile 修飾(多線程下保證內(nèi)存可見性)?
接下來大家可以看到 JDK8 新增原子操作類 LongAdder,在非常高的并發(fā)請求下,AtomicLong 的性能會受影響,這是因為雖然 AtomicLong 使用無數(shù) CAS 算法,但是 CAS 失敗后還是通過無限循環(huán)的自旋鎖不斷嘗試的。在高并發(fā)下 N 多線程同時去操作一個變量,會造成大量線程 CAS 失敗,然后處于自旋狀態(tài),這大大浪費了 cpu 資源。
既然 AtomicLong 性能是由于過多線程同時去競爭一個變量的更新而降低的,那么如果把一個變量分解為多個變量,讓同樣多的線程去競爭多個資源,性能問題不就解決了?JDK8 提供的 LongAdder 就是這個思路。看到這里大家或許會眼前一亮。
最后大家可以去看一下,比較簡單的并發(fā)安全基于寫時拷貝的 CopyOnWriteArrayList 的實現(xiàn),以及探究其迭代器的弱一致性實現(xiàn)原理(即寫時拷貝)。
接下來進入核心環(huán)節(jié),也就是對 JUC 包中鎖的研究。
一開始要先把 LockSupport 類研究透,即:鎖中讓線程掛起與喚醒的基礎設施。由于鎖是基于 AQS(AbstractQueuedSynchronizer)實現(xiàn)的,所以肯定要先把 AQS 搞清楚。
你將會發(fā)現(xiàn) AQS ?中維持了一個單一的狀態(tài)信息 state, 可以通過 getState,setState,compareAndSetState 函數(shù)修改其值。
對于 ReentrantLock 的實現(xiàn)來說,state 可以用來表示當前線程獲取鎖的可重入次數(shù);對于讀寫鎖 ReentrantReadWriteLock 來說,state 的高 16 位表示讀狀態(tài),也就是獲取該讀鎖的次數(shù),低 16 位表示獲取到寫鎖線程的可重入次數(shù);對于 semaphore 來說,state 用來表示當前可用信號的個數(shù);對于 FutuerTask 來說,state 用來表示任務狀態(tài)(例如還沒開始,運行,完成,取消);對于 CountDownlatch 和 CyclicBarrie 來說,state 用來表示計數(shù)器當前的值。
AQS 有個內(nèi)部類 ConditionObject 是用來結(jié)合鎖實現(xiàn)線程同步,ConditionObject 可以直接訪問 AQS 對象內(nèi)部的變量,比如 state 狀態(tài)值和 AQS 隊列。ConditionObject 是條件變量,每個條件變量對應著一個條件隊列 (單向鏈表隊列),用來存放調(diào)用條件變量的 await() 方法后被阻塞的線程。
AQS 類并沒有提供可用的 tryAcquire 和 tryRelease,正如 AQS 是鎖阻塞和同步器的基礎框架,tryAcquire 和 tryRelease 需要有具體的子類來實現(xiàn)。子類在實現(xiàn) tryAcquire 和 tryRelease 的時候,要根據(jù)具體場景使用 CAS 算法嘗試修改狀態(tài)值 state, 成功則返回 true, 否則返回 false。子類還需要定義在調(diào)用 acquire 和 release 方法的時候 ,state 狀態(tài)值的增減代表什么含義。
比如繼承自 AQS 實現(xiàn)的獨占鎖 ReentrantLock,定義當 status 為 0 的時候表示鎖空閑;為 1 的時候表示鎖已經(jīng)被占用。在重寫 tryAcquire 的時候,內(nèi)部需要使用 CAS 算法,查看當前 status 是否為 0,如果為 0 則使用 CAS 設置為 1,并設置當前線程的持有者為當前線程,返回 true;如果 CAS 失敗則返回 false。
ReentrantLock?在實現(xiàn) tryRelease 的時候,內(nèi)部需要使用 CAS 算法把當前 status 的值從 1 修改為 0,并設置當前鎖的持有者為 null,然后返回 true, 如果 cas 失敗則返回 false。
知道 AQS 是什么后,下面先看最簡單的獨占鎖 ReentrantLock。你可以先畫出其類圖結(jié)構(gòu),看看有哪些變量和方法,將會發(fā)現(xiàn)它有著公平鎖與獨占鎖之分(回顧基礎篇)。
類圖中狀態(tài)值 state 代表線程獲取該鎖的可重入次數(shù),當一個線程第一次獲取該鎖時, state 的值為 0;第二次獲取后,該鎖狀態(tài)值為 1,這就是可重入次數(shù)。然后加大難度,看看讀寫鎖 ReentrantReadWriteLock 是怎么實現(xiàn)讀寫分離、增加并發(fā)度的,別忘了還有 JDK 新增的 StampedLock 。
等鎖研究完了,就可以對并發(fā)隊列進行研究了。其中,隊列要分為基于 CAS 的無阻塞隊列 ConcurrentLinkedQueue ?和其他基于鎖的阻塞隊列。先看比較簡單的 ArrayBlockingQueue,LinkedBlockingQueue,ConcurrentLinkedQueue,別忘了還有高級的優(yōu)先級隊列 PriorityBlockingQueue 和延遲隊列 DelayQueue。
好像少了線程池?線程池主要解決兩個問題:
- 當執(zhí)行大量異步任務的時候,線程池能夠提供較好的性能;在不使用線程池且需要執(zhí)行異步任務時,直接 new 一線程進行運行,線程的創(chuàng)建和銷毀是需要開銷的。線程池里面的線程是可復用的,不會每次執(zhí)行異步任務時候都重新創(chuàng)建和銷毀線程;
- 線程池提供了一種資源限制和管理的手段。比如可以限制線程的個數(shù)、動態(tài)新增線程等,每個 ThreadPoolExecutor 也保留了一些基本的統(tǒng)計數(shù)據(jù),如:當前線程池完成的任務數(shù)目等。
前面講解過 Java 中線程池 ThreadPoolExecutor 原理的探究,ThreadPoolExecutor 是 Executors 工具類里的一部分功能。下面介紹另外一部分功能,也就是 ScheduledThreadPoolExecutor 的實現(xiàn),它是一個可以指定一定延遲時間后或者定時進行任務調(diào)度執(zhí)行的線程池。
JUC 中重要的高級線程同步器 CountDownLatch、CyclicBarrier、Semaphore 也不能忽略,這些高級的同步器會大大簡化我們編寫線程同步任務的門檻、降低我們的出錯率。
雖然 Java 并發(fā)編程內(nèi)容很廣,但還是有一些規(guī)則可以遵循,比如線程。線程池創(chuàng)建的時候要指定名稱以便排查問題,線程池使用完畢記得關閉,ThreadLocal 使用完畢記得調(diào)用 remove 清理,SimpleDateFormat 類是線程不安全的等等。
總結(jié)
如果你對上面的內(nèi)容感興趣,但對學并發(fā)無從下手,那么機會來了!《Java并發(fā)編程之美》這本書,就是按照以上的思路來編寫的,該書在京東上被列為 10 大精選書籍之一。
購買鏈接:https://item.m.jd.com/product/12450812.html
掃描下方二維碼添加小助手,與 8000 位云原生愛好者討論技術(shù)趨勢,實戰(zhàn)進階!
進群暗號:公司-崗位-城市
關注阿里巴巴云原生公眾號,后臺回復關鍵字“并發(fā)”,即可參與送書抽獎!**
總結(jié)
以上是生活随笔為你收集整理的Java 并发编程-不懂原理多吃亏(送书福利)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Kubernetes 弹性伸缩全场景解读
- 下一篇: 从入门到实践:创作一个自己的 Helm