業務開發過程,其實就是用戶業務數據的處理過程,於是開發的核心任務就是維護數據一致不出錯。現實場景中,多個用戶會併發讀寫同一份數據(如秒殺),不加控制會翻車、加了控制則下降併發度,影響性能和用戶體驗。java
如何優雅的進行併發數據控制呢?本質上須要解決兩個問題:數組
- 讀-寫衝突
- 寫-寫衝突
讓咱們看下Java經典的併發容器CopyOnWriteList以及ConcurrentHashMap是如何協調這兩個問題的數據結構
CopyOnWrite顧名思義即寫時複製策略多線程
針對寫處理,首先加ReentrantLock鎖,而後複製出一份數據副本,對副本進行更改以後,再將數據引用替換爲副本數據,完成後釋放鎖併發
針對讀處理,依賴volatile提供的語義保證,每次讀都能讀到最新的數組引用app
顯然,CopyOnWriteList採用讀寫分離的思想解決併發讀寫的衝突高併發
當讀操做與寫操做同時發生時:post
可見在讀寫分離的設計下,併發讀寫過程當中,讀不必定能實時看到最新的數據,也就是所謂的弱一致性。性能
也正是因爲犧牲了強一致性,可讓讀操做無鎖化,支撐高併發讀學習
當多個寫操做的同時發生時,先拿到鎖的先執行,其餘線程只能阻塞等到鎖的釋放
簡單粗暴又行之有效,但併發性能相對較差
主要採用分段鎖的思想,下降同時操做一份數據的機率
針對讀操做:
針對寫操做:
若併發讀寫的數據不位於同一個Segment,操做是相互獨立的
若位於同一個Segment,ConcurrentHashMap利用了不少Java特性來解決讀寫衝突,使得不少讀操做都無鎖化
當讀操做與寫操做同時發生時:
可見,支持無鎖併發讀操做仍是弱一致的
若併發寫操做的數據不位於同一個Segment,操做是相互獨立的
若位於同一個Segment,多個線程仍是因爲加ReentrantLock鎖致使阻塞等待
與JDK7相比,少了Segment分段鎖這一層,直接操做Node數組(鏈表頭數組),後面稱爲桶
針對讀操做,經過UNSAFE.getObjectVolatile原子讀語義獲取最新的value
針對寫操做,因爲採用懶惰加載的方式,剛初始化時只肯定桶的數量,並無初始默認值。當須要put值的時候先定位下標,而後該下標下桶的值是否爲null,若是是,則經過UNSAFE.comepareAndSwapObject(CAS)賦值,若是不爲null,則加Synchronized鎖,找到對應的鏈表/紅黑樹的節點value進行更改,後釋放鎖
若併發讀寫的數據不位於同一個桶,則相互獨立互不干擾
若位於同一個桶,與JDK7的版本相比,簡單了許多,但仍是基於Java的特性使得許多讀操做無鎖化
當讀操做與寫操做同時發生時:
所以只要寫操做happens-before讀操做,volatile語義就能夠保證讀的數據是最新的,能夠說JDK8版本的ConcurrentHashMap是強一致的(此處只關注基本讀寫(GET/PUT),可能會有弱一致的場景遺漏,例如擴容操做,不過應該是全局加鎖的,若有錯誤煩請指出,共同窗習)
若併發讀寫的數據不位於同一個桶,則相互獨立互不干擾
若位於同一個桶,注意到寫操做在不一樣的場景下采起不一樣的策略,CAS或Synchronized
當多個寫操做同時發生時,若桶爲null,則CAS應對併發寫,當第一個寫操做賦值成功後,後面的寫線程CAS失敗,轉爲競爭Synchronized鎖,阻塞等待
對數據進行存儲必然涉及數據結構的設計,任何對數據的操做都得基於數據結構
常規思路是對整個數據結構加鎖,可是鎖的存在會大大影響性能,因此接下來的任務,就是找到哪些能夠無鎖化的操做
操做主要分爲兩大類,讀和寫。
先看寫,由於涉及到原有數據的改動,不加控制確定會翻車,怎麼控制呢?
寫操做也分兩種,一種會改變結構,一種不會
對於會改變結構的寫,無論底層是數組仍是鏈表,因爲改動得基於原有的結構,必然得加鎖串行化保證原子操做,優化的點就是鎖層面的優化了,例如最開始HashTable等synchronized鎖到ConcurrentHashMap1.7版本的ReentrantLock鎖,再到1.8版本的Synchronized改良鎖 。或者數據分散化,concurrnethashmap等基於hash的數據結構比CopyOnWriteList的數據結構就多了桶分散的優點
對於不會改變結構的寫,或者改動的頻率不大(桶擴容頻率低),因爲鎖的開銷實在是太大了,CAS是個不錯的思路。爲何CopyOnWriteList不用CAS來控制併發寫,我我的以爲主要緣由仍是由於結構變化頻繁,能夠看下ActomicReferenceArray等基於CAS的數組容器,都是建立後就不容許結構發生變化的。
確保數據不會改錯以後,讀相對就好辦了
主要考慮是否是要實時讀最新的數據(等待寫操做完成),也就是強一致仍是弱一致的問題
強一致的話,讀就得等寫完成,讀寫競爭同一把鎖,這就相互影響了讀寫的效率。
大多數場景下,讀的數據一致性要求沒有寫的要求高,能夠讀錯,可是堅定不能夠寫錯。要是在讀的這一刻,數據還沒改完,讀到舊數據也不要緊,只要最後寫完對讀可見便可
還好JMM(Java內存模型)有個volatile可見性的語義,能夠保證不加鎖的狀況下,讀也能看到寫更改的數據。此外還有UNSAFE包的各類內存直接操做,也可相對高性能的完成可見性語義
對讀操做而言,最好的數據,就是不變的數據,不用擔憂被修改引起的各類問題。惟一的不變是變化,一些數據仍是有變化的可能,若是要支持這種不變性,或者說盡可能減小變化的頻率,變化的部分就得在別的地方處理,也就是所謂的讀寫分離
以上純我的理解,受限於水平,想法不必定正確,歡迎討論指點
Java進階(六)從ConcurrentHashMap的演進看Java多線程核心技術