前言java
本篇博客將對線程併發的一些基礎知識進行闡述,你們也能夠參考樓主之前關於線程的2篇博客:《Java多線程感悟一》、《Java多線程感悟二》 。
編程
CPU、進程、線程安全
咱們知道進程是操做系統進行資源分配的最小單位,一個進程內部能夠有多個線程進行資源的共享,線程做爲CPU調度的最小單位,CPU會依據某種原則(好比時間片輪轉)對線程進行上下文切換,從而併發執行多個線程任務。打個比喻,CPU就像高速公路同樣,每條高速公路會有並排的車道,而線程就像在路上行駛的汽車同樣。咱們能夠經過/proc/cpuinfo來查看服務器有幾個CPU,以及每一個CPU支持的核心線程數,這樣咱們就瞭解了服務器有幾條高速公路,以及每條高速公路有幾個並排的車道。
服務器
多線程引起的思考多線程
粗粒度的來說,JAVA對內存的劃分能夠分爲:堆和棧。對於多線程而言,堆就好像主內存,而棧就像是工做內存。堆是多線程共享的,線程工做時要將堆中的數據 COPY TO 工做內存才能進行工做。而線程何時COPY DATA TO工做內存?工做內存中的數據計算完畢又何時寫回主內存?當多個線程之間對共享的數據進行讀寫,那麼這一瞬間的讀寫是個什麼順序呢?一個線程可否看到或者何時才能看到另外一個線程的改變呢?讀和讀的線程是否不須要控制併發呢?當有寫線程參與時,對讀線程有什麼影響呢?寫線程存在時,讀線程是否必定要等呢?線程須要完成一連串的讀寫操做,是否容許其餘線程插入進來呢?併發
多線程的基礎:可見性ide
正如上面所言,因爲存在主內存以及工做內存,每個線程都是在本身的工做內存中進行工做的,若是線程在本身的區域埋頭苦幹,殊不知道其餘線程已經對共享的數據作出修改,這將會引起「可見性」問題。好比咱們有一個配置文件,有一個寫線程會對配置參數進行修改,其餘不少讀線程讀取配置進行業務上的計算,若是寫線程修改了,但是讀線程依舊按照老的配置進行,也不知道讀線程何時能「醒悟」,這多麼可怕!固然JAVA已經爲咱們提供了輕量級的volatile來解決這個問題。(volatile不只僅提供可見性,並且對於CPU/編譯器優化帶來的代碼重排性也作了限制)高併發
不只僅可見性能
可見,這只不過是一瞬間的事情,更多時候,咱們要的是一段時間內的操做的封閉性,即原子性。一個對象,它能夠執行不少代碼,可是咱們但願它在執行某段代碼(即臨界區)時可以有一些限制,好比只容許一個線程對這個對象進行這段代碼的操做,第二個線程要想操做必須等待第一個線程結束後。說的直白點,這個對象就好像一把鎖,它存在3個臨界區,那麼這個對象在任意時刻只能處在一個臨界區內!優化
synchronized
經過synchronized來對對象的代碼進行臨界區劃分,從而完成可見性以及原子性的要求。synchronized是隱式的鎖方式,由於加鎖和解鎖的過程是JAVA幫助咱們來進行的,無需咱們關心。正是因爲這種隱式的方式,咱們應重點關注的是synchronized鎖住的是什麼?鎖住的是對象?仍是鎖住的是對象的臨界區?鎖對象的生命週期是什麼?鎖對象的粒度多大,是否能夠優化?是否由於鎖對象的粒度太大致使代碼的串行,使得系統效率低下?
Lock
synchronized是JAVA最爲古老的,也在不斷優化的鎖機制,在JAVA發展過程當中也推出了新的鎖機制:Lock。Lock是顯式的鎖,須要手動的上鎖以及解鎖。特別須要注意的是必須fiannly解鎖,不然會出現死鎖現象。第一個經常使用的鎖是:ReentrantLock ,這是一個排他鎖,和synchronized功能相似,無論線程是讀,仍是寫,都是互斥的。第二個經常使用的鎖是:ReentrantWriteReadLock,這是讀寫鎖,若是讀,用readLock,若是寫,用writeLock,從而達到讀與讀的併發,讀寫之間的互斥。
Atomic與CAS機制
不少時候,咱們僅僅但願對某個變量作一系列簡單的動做,但願保證可見性以及原子性的操做,JAVA已經爲咱們提供了Atomic相關的類,使用最爲普遍的就是AtomicInteger。這類Atomic雖然沒有利用synchronized/Lock這樣的鎖機制,可是經過CAS達到了一樣的目的。看一段AtomicInteger的代碼:
一段死循環,先獲取old值,而後嘗試對比修改成新值,雖然沒有臨界區的鎖控制,多個線程併發進行修改,可是顯然compareAndSet保證了只會有一個線程能成功(至關於得到鎖),這就是CAS機制。若是咱們將死循環改爲有限幾回嘗試CAS修改的話,就是本身設置了自旋的次數了。
用空間換時間:CopyOnWrite機制
在前文涉及的鎖機制,都沒法避免一個問題:一旦存在寫線程,那麼讀線程勢必沒法併發進行。那麼能否讓讀寫併發進行呢?
CopyOnWrite機制:對於一個容器而言,多個讀線程能夠併發的讀取該容器的內容;若是存在寫線程,那麼先COPY一份此容器,寫線程對COPY的容器進行操做,待寫線程操做完畢後,將老的容器的引用重置爲COPY後的容器。這樣一來,讀寫線程操做的容器不是同一個容器,固然能夠併發進行操做。經過Copy的機制,利用空間來換取時間,須要注意的是當大量存在寫線程時對內存的消耗。
併發編程集合類
StringBuffer 和 StringBuilder
StringBuffer的方法都打上了synchronized標籤,天然是線程安全的;後來JDK走了一個極端,爲咱們提供了StringBuilder這樣的非線程安全類,在單線程的環境下,提高了性能。
Hashtable 、 HashMap 、ConcurrentHashMap
Hashtable和HashMap同上面的StringBuffer/StringBuilder同樣。
後來JDK出現了java.util.concurrent併發包,好比ConcurrentHashMap就經過分解鎖的粒度,提升併發能力。下面咱們來仔細剖析下ConcurrentHashMap的實現原理:
對於Hashtable/HashMap而言,其實裏面存放的K/V並無分層處理,對於Hashtable而言,若是鎖,那麼意味着鎖住整個Hashtable的內容,意味着就算是讀與讀也得串行進行。而ConcurrentHashMap則將K/V進行劃分,多個K/V成爲一個segment,默認有16個segment,顯然不一樣segment之間的讀寫能夠併發進行,天然將鎖的粒度一會兒下降16倍。在每一個segment內部,實際上藉助於extends ReentrantLock實現讀寫互斥;而不一樣segment之間則不存在互斥關係。
CopyOnWriteArrayList 、 CopyOnWriteArraySet 、ArrayList 、Vector
Vector和ArrayList相似於StringBuffer/StringBuilder同樣。
咱們來看一段CopyOnWriteArrayList的代碼,揭開CopyOnWrite機制:
add時利用排他鎖達到互斥,在代碼中能夠看到Arrays.copyOf進行COPY,增長完元素後,利用setArray達到引用重置的目的。
再來看看獲取元素的代碼:
能夠看到,沒有鎖的限制,讀寫併發進行操做!