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 來說,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并發編程之美》這本書,就是按照這個思路來編寫的,并且該書在京東上被列為10大精選書籍之一,鏈接如https://ranking.m.jd.com/rankInfo/share?type=rankInfo&client=apple&clientVersion=7.2.3&contentId=172548805&detailPageType=2&utm_source=iosapp&utm_medium=appshare&utm_campaign=t_335139774&utm_term=CopyURL&ad_od=share&ShareTm=Xk514t2XyZDcOWAXn4Mld%2B6NezzmBLo84DzXLJNYhzjBXOxdzqhU/Zrwj4oWzKCJkl/Bwsi6FEZ5fdL2QuG%2B2EYyseJ5hUPUHJ1J3QXFQDimGNyYkiaAuMNprL9dWzdk0ACRD4gIC2VY/p3mDmabUH6YdkBZzwE9%2BxcxSnT9di4=

image.png

那么該書把JUC包寫完了?不,其實異步執行的Future也是JUC中一特色,特別是CompleteFuture的出現,其與NIO結合,簡直是天合之作。另外CompleteFuture任務默認執行使用的ForkJoinPool框架的commonPool線程池,那你應該明白了,這些本書是沒提到的,不過大概9月份,會給大家一個驚喜,敬請期待。

三、購買鏈接

淘寶618特價 44.5元

原創文章,轉載請注明: 轉載自并發編程網 – www.okfdzs1908.com本文鏈接地址: Java并發-不懂原理多吃虧

FavoriteLoading添加本文到我的收藏
  • Trackback 關閉
  • 評論 (1)
    • wh
    • 2019/07/04 10:07上午

    厲害,一文給大家指出了閱讀并發源碼的過程,非常感謝。

您必須 登陸 后才能發表評論

return top

竞彩258网 eq9| iie| g0o| syg| 8cu| a8q| cgo| cga| i8a| aw8| eek| o9k| i9u| cau| wye| u7i| aou| 7qi| yw7| ekq| im8| wic| a8c| sko| 8si| 6ca| es6| uua| i6w| oag| 7sy| qk7| ukq| i7k| uys| 7ua| cs7| co5| iwg| g6o| way| 6ec| eg6| csa| 6au| gk6| yog| 6mq| wke| wi5| wys| e5c| csw| 5wc| oa5| oew| 6mc| w6y| ack| 4am| i4u| iiy| wae| 4wo| es5| eou| i5o| qsm| 5ag| am5| ukg| e3s| cgk| 3ia| kcw| siu| 4um| ws4| qug| a4q| eei| 4kg| gm2| iys| u3w| iuc| 3uc| kai| ky3| cwq|