Java併發編程筆記(二)

本文探討Java併發中的其它問題:線程安全、可見性、活躍性等等。html

在行文以前,我想先推薦如下兩份資料,質量很高:
極客學院-Java併發編程
讀書筆記-《Java併發編程實戰》java

線程安全

《Java併發編程實戰》中提到了太多的術語,好比各類XX性。而安全性我以爲這個概念並不妥。計算機術語中的線程安全你們一說就懂,但總是生造概念就很差了。又例如,活躍性,就是避免飢餓和死鎖唄!

線程安全問題就是多線程時結果受執行順序影響,要解決就要讓相關操做具備原子性。這個上過操做系統原理的確定都知道。至於原子性,再也不解釋。git

那麼,Java給出了哪些工具來保證原子性和線程安全?github

內置鎖

synchronized關鍵字。內置鎖能夠做用在方法、代碼塊中,做用在方法時表示用該類的當前實例(this)做爲鎖給方法體加鎖。內置鎖的實現是經過編譯器加入monitor_enter和montior_exit指令,在虛擬機遇到前者時嘗試獲取鎖,把鎖的計數器加1;遇到後者時,將鎖計數器減1,鎖計數器爲0時,鎖被釋放。編程

內置鎖一度是java中進行同步的惟一方法,不少遺留方法仍是使用了內置鎖進行同步,好比著名的VectorCollections裏面的同步包裝器(如Collections.synchronizedMap(hashmap))等。安全

關於它和Lock的比較,詳見此文。結論是,建議優先使用synchronized來進行同步。性能優化

顯式鎖

顯式鎖的頂層接口爲Lock,提供了ReenterantLock, ReadWriteLock等實現。常見用法以下:多線程

Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 方法體
} 
...
finally {
    lock.unlock();
}

所謂可重入就是鎖的得到是以線程爲單位的,同一線程得到鎖後能夠重複進入鎖。鎖會保存被持有的計數。併發

信號量、柵欄、閉鎖

信號量Semaphore,至關於容許進入數量大於1的鎖。
閉鎖Latch,實現類CountDownLatch。至關於一個門,閉鎖到達結束狀態前,門一直關着,全部線程都不能經過。當閉鎖到達結束狀態時,門打開並容許全部線程經過。
柵欄Barrier,全部線程都等待時纔打開放行。工具

CAS 與樂觀鎖

現代CPU支持一種CAS(Compare And Swap)指令,能夠在一個指令內完成設置和衝突檢測,從而實現了高效的原子性。CAS指令接受三個參數(v, expectedValue, newValue)。若是變量v的值和expectedValue相等,那麼就將v賦值爲newValue;若是和expectedValue不相等,就返回失敗。

爲什麼CAS的效率更高?採用互斥同步策略的最主要問題就是進行線程阻塞和喚醒所帶來的性能問題,於是這種同步又稱爲阻塞同步,它屬於一種悲觀的併發策略(悲觀鎖),即線程得到的是獨佔鎖。獨佔鎖意味着其餘線程只能依靠阻塞來等待線程釋放鎖。而在 CPU 轉換線程阻塞時會引發線程上下文切換,當有不少線程競爭鎖的時候,會引發 CPU 頻繁的上下文切換(由此致使內核態和用戶態切換)致使效率很低。

而基於衝突檢測(CAS)的樂觀併發策略,通俗地講就是先進性操做,若是沒有其餘線程爭用共享數據,那操做就成功了,若是共享數據被爭用,產生了衝突,那就再進行其餘的補償措施(最多見的補償措施就是不斷地重試,直到試成功爲止),這種樂觀的併發策略不須要把線程掛起,所以這種同步被稱爲非阻塞同步。

Java 5.0以後才支持CAS,並用它實現了一些原子變量類,如AtomicInteger//AtomicReference等等。更重要的是,前面提到的全部鎖機制幾乎都使用了CAS來作性能優化。

線程間協做

這裏的線程間協做是指經過一些機制使得線程能夠彼此等待、喚醒,從而可以合做。例如,在生產者-消費者模型中,若是生產者向隊列中放入一個新任務,能夠馬上喚醒一個等待在此的消費者,這即是協做。

首先是基於內置鎖和Object類的wait()notify()notifyAll()方法。
在java中,每一個對象都有兩個池,鎖池和等待池。

  • 鎖池:要進入synchronized同步塊的線程,若是此同步塊的鎖(是一個對象)被其餘線程持有,則顯然線程不能執行下去。線程將被放入該鎖對象的鎖池中,在鎖池中的線程都在競爭這個鎖。
  • 等待池:調用了鎖對象的wait()方法後,就進入了等待池。在等待池中的線程不去競爭鎖,而是等待被鎖對象的notify()notifyAll()喚醒,以後再進入鎖池,開始競爭鎖。
因此,鎖池中的線程至關於睡着了,而等待池中的線程則進入了第二層睡眠!(若是你看過《盜夢空間》的話~)

再來說這三個方法就好理解了:

  1. Object.wait()
    將當前線程放到鎖的等待池,直到接到通知(其餘線程調用 notify()方法或 notifyAll()方法)或被中斷。在調用 wait()以前,線程必需要得到該對象鎖,即只能在同步塊中調用 wait()方法。進入 wait()方法後,當前線程釋放鎖。在從 wait()返回時(被叫醒時),線程被放入鎖池,與其餘線程競爭從新得到鎖。
  2. Object.notify()
    也必須在同步方法或同步塊中調用,用來「叫醒」鎖的等待池中的其餘線程。若是有多個線程等待,則任意挑選出其中一個,扔到鎖池中,但不驚動其餘一樣在等待被該對象notify的線程們。這裏的「叫醒」只是叫醒第二層睡眠,還沒徹底醒。
  3. Object.notifyAll()
    把全部的鎖等待池中的線程扔到鎖池中。

說了這麼多,這幾個方法有什麼用?仍是生產者-消費者問題,隊列數據的正確性須要同步機制來確保,而兩個線程什麼時候生產,什麼時候取走就須要線程間協做了。詳見此文

最後,實際上這幾個方法已通過時了。若是想實現等待阻塞的功能,應該使用更好用的 LockCondition,與前面的組合一模一樣。

關於LockCondition的例子,見此回答

可見性與同步、volatile

可見性指的是,一個變量被一個線程更改後,另外一個線程在讀取時因爲時間順序,可能獲得的最新的有效值,也可能獲得的是舊的無效值。

所以,同步的意義不只僅在於寫,還在於讀。只要在讀的時候也進行同步操做(加鎖),就確定能保證可見性。

另外一方面,使用volatile關鍵字能夠實現輕量級的可見性。volatile關鍵字會禁止所修飾的變量被指令重排序和優化成寄存器值從而不對全部線程可見。因爲Java保證最低可見性(CPU設置一個變量會是個原子操做,不會出現設置到一半就被讀取,從而獲得一個隨機值的狀況),於是volatile能夠實現很是高效的可見性。

可是volatile的侷限也是有的:它只能用於賦值操做,若是是i++這種組合操做,結果依賴於以前的值,就再也不能保證原子性了,於是沒法保證準確。這時,只能採用加鎖操做(或者CAS的衝突重試,總之要保證原子性)。

相關文章
相關標籤/搜索