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