做者 | 加多
關注阿里巴巴雲原生公衆號,後臺回覆關鍵字「併發」,便可參與送書抽獎!
**
html
導讀:併發編程與 Java 中其餘知識點相比較而言學習門檻較高,從而致使不少人望而卻步。但不管是職場面試,仍是高併發/高流量系統的實現,都離不開併發編程,因而可以真正掌握併發編程的人成爲了市場迫切需求的人才。本文中,做者加多以通俗易懂的方式講解了多線程併發編程從入門到實踐須要掌握的理論知識與實際操做方法。面試
Java 併發編程做爲 Java 技術棧中的一根頂樑柱,其學習成本仍是比較大的,不少人學習起來感到沒有頭緒、無從下手。那麼學習併發編程是否有一些技巧在裏面呢?
爲了讓開發者從 Java 併發編程的苦海中解脫出來,大神 Doug Lea 特地爲 Java 開發人員作了一件事情,那就是在 JDK 中提供了 Java 併發包(JUC)。
該包提供了經常使用的併發相關的工具類,好比鎖、併發安全的隊列、併發安全的列表、線程池、線程同步器等。有了 JUC 包,開發人員編寫併發程序的時候,就再也不那麼吃力了;可是工具雖好,若是你對其原理不瞭解,仍是很容易犯錯,即:不懂原理多吃虧。
下面爲你們舉三個例子進行說明:
算法
上面的幾個例子,意在說明雖然有了 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 來講,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。
ReentrantLock 在實現 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。
好像少了線程池?線程池主要解決兩個問題:
編程
前面講解過 Java 中線程池 ThreadPoolExecutor 原理的探究,ThreadPoolExecutor 是 Executors 工具類裏的一部分功能。下面介紹另一部分功能,也就是 ScheduledThreadPoolExecutor 的實現,它是一個能夠指定必定延遲時間後或者定時進行任務調度執行的線程池。
JUC 中重要的高級線程同步器 CountDownLatch、CyclicBarrier、Semaphore 也不能忽略,這些高級的同步器會大大簡化咱們編寫線程同步任務的門檻、下降咱們的出錯率。
雖然 Java 併發編程內容很廣,但仍是有一些規則能夠遵循,好比線程。線程池建立的時候要指定名稱以便排查問題,線程池使用完畢記得關閉,ThreadLocal 使用完畢記得調用 remove 清理,SimpleDateFormat 類是線程不安全的等等。
安全
若是你對上面的內容感興趣,但對學併發無從下手,那麼機會來了!《Java併發編程之美》這本書,就是按照以上的思路來編寫的,該書在京東上被列爲 10 大精選書籍之一。
購買連接:https://item.m.jd.com/product/12450812.html
多線程
掃描下方二維碼添加小助手,與 8000 位雲原生愛好者討論技術趨勢,實戰進階!
進羣暗號:公司-崗位-城市併發
關注阿里巴巴雲原生公衆號,後臺回覆關鍵字「併發」,便可參與送書抽獎!**
框架