樂觀鎖、悲觀鎖、公平鎖、自旋鎖、偏向鎖、輕量級鎖、重量級鎖、鎖膨脹...難理解?不存的!來,話很少說,帶你飆車。html
上一篇介紹了線程池的使用,在享受線程池帶給咱們的性能優點以外,彷佛也帶來了另外一個問題:線程安全的問題。java
那什麼是線程的安全問題呢?編程
線程安全問題:指的是在多線程編程中,同時操做同一個可變的資源以後,形成的實際結果與預期結果不一致的問題。安全
好比:A和B同時向C轉帳10萬元。若是轉帳操做不具備原子性,A在向C轉帳時,讀取了C的餘額爲20萬,而後加上轉帳的10萬,計算出此時應該有30萬,但還將來及將30萬寫回C的帳戶,此時B的轉帳請求過來了,B發現C的餘額爲20萬,而後將其加10萬並寫回。而後A的轉帳操做繼續——將30萬寫回C的餘額。這種狀況下C的最終餘額爲30萬,而非預期的40萬。多線程
若是上面的內容您尚未理解,不要緊,咱們來看下面非安全線程的模擬代碼:併發
public class ThreadSafeSample { public int number; public void add() { for (int i = 0; i < 100000; i++) { int former = number++; int latter = number; if (former != latter-1){ System.out.printf("非相等 former=" + former + " latter=" + latter); } } } public static void main(String[] args) throws InterruptedException { ThreadSafeSample threadSafeSample = new ThreadSafeSample(); Thread threadA = new Thread(new Runnable() { @Override public void run() { threadSafeSample.add(); } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { threadSafeSample.add(); } }); threadA.start(); threadB.start(); threadA.join(); threadB.join(); } }
我電腦運行的結果: 非相等 => former=5555 latter=6061
ide
能夠看到,僅僅是兩個線程的低度併發,就很是容易碰到 former 和 latter 不相等的狀況。這是由於,在兩次取值的過程當中,其餘線程可能已經修改了number.性能
線程安全的解決方案分爲如下幾個維度(參考《碼出高效:Java開發手冊》):優化
解決線程安全核心思想是:「要麼只讀,要麼加鎖」,解決線程安全的關鍵在於合理的使用Java提供的線程安全包java.util.concurrent簡稱JUC。this
Java 5 之前,synchronized是僅有的同步手段,Java 5的時候增長了ReentrantLock(再入鎖)它的語義和synchronized基本相同,比synchronized更加靈活,能夠作到更多的細節控制,好比鎖的公平性/非公平性指定。
synchronized 是 Java 內置的同步機制,它提供了互斥的語義和可見性,當一個線程已經獲取當前鎖時,其餘試圖獲取的線程只能等待或者阻塞在那裏。
synchronized 能夠用來修飾方法和代碼塊。
synchronized (this) { int former = number++; int latter = number; //... }
public synchronized void add() { //... }
synchronized 是由一對 monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現單元。在 Java 6 以前,Monitor的實現徹底是依靠操做系統內部的互斥鎖,由於須要進行用戶態到內核態的切換,因此同步操做是一個無差異的重量級操做,性能也很低。但在Java 6的時候,JVM 對此進行了大刀闊斧地改進,提供了三種不一樣的 Monitor 實現,也就是常說的三種不一樣的鎖:偏向鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。
偏向鎖是爲了解決在沒有多線程的訪問下,儘可能減小鎖帶來的性能開銷。
輕量級鎖是指當鎖是偏向鎖的時候,被另外一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,提升性能。
重量級鎖是指當鎖爲輕量級鎖的時候,另外一個線程雖然是自旋,但自旋不會一直持續下去,當自旋必定次數的時候,尚未獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其餘申請的線程進入阻塞,性能下降。
Java 6 以後優化了 synchronized 實現方式,使用了偏向鎖升級爲輕量級鎖再升級到重量級鎖的方式,減低了鎖帶來的性能消耗,也就是咱們常說的鎖膨脹或者叫鎖升級,那麼它是怎麼實現鎖升級的呢?
鎖膨脹(升級)原理: 在鎖對象的對象頭裏面有一個ThreadId字段,在第一次訪問的時候ThreadId爲空,JVM讓其持有偏向鎖,並將ThreadId設置爲其線程id,再次進入的時候會先判斷ThreadId是否尤爲線程id一致,若是一致則能夠直接使用,若是不一致,則升級偏向鎖爲輕量級鎖,經過自旋循環必定次數來獲取鎖,不會堵塞,執行必定次數以後就會升級爲重量級鎖,進入堵塞,整個過程就是鎖膨脹(升級)的過程。
自旋鎖是指嘗試獲取鎖的線程不會當即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減小線程上下文切換的消耗,缺點是循環會消耗CPU。
悲觀鎖和樂觀鎖並非某個具體的「鎖」而是一種是併發編程的基本概念。
悲觀鎖認爲對於同一個數據的併發操做,必定是會發生修改的,哪怕沒有修改,也會認爲修改。所以對於同一個數據的併發操做,悲觀鎖採起加鎖的形式。悲觀的認爲,不加鎖的併發操做必定會出問題。
樂觀鎖則與 Java 併發包中的 AtomicFieldUpdater 相似,也是利用 CAS 機制,並不會對數據加鎖,而是經過對比數據的時間戳或者版本號,來實現樂觀鎖須要的版本判斷。
公平鎖是指多個線程按照申請鎖的順序來獲取鎖。
非公平鎖是指多個線程獲取鎖的順序並非按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。
若是使用 synchronized 使用的是非公平鎖,是不可設置的,這也是主流操做系統線程調度的選擇。通用場景中,公平性未必有想象中的那麼重要,Java 默認的調度策略不多會致使 「飢餓」發生。非公平鎖的吞吐量大於公平鎖。
非公平鎖吞吐量大於公平鎖的緣由:
好比A佔用鎖的時候,B請求獲取鎖,發現被A佔用以後,堵塞等待被喚醒,這個時候C同時來獲取A佔用的鎖,若是是公平鎖C後來者發現不可用以後必定排在B以後等待被喚醒,而非公平鎖則可讓C先用,在B被喚醒以前C已經使用完成,從而節省了C等待和喚醒之間的性能消耗,這就是非公平鎖比公平鎖吞吐量大的緣由。
ReentrantLock只能修飾代碼塊,使用ReentrantLock必須手動unlock釋放鎖,否則鎖永遠會被佔用。
ReentrantLock reentrantLock = new ReentrantLock(true); // 設置爲true爲公平鎖,默認是非公平鎖 reentrantLock.lock(); try { }finally { reentrantLock.unlock(); }
具有嘗試非阻塞地獲取鎖的特性:當前線程嘗試獲取鎖,若是這一時刻鎖沒有被其餘線程獲取到,則成功獲取並持有鎖;
能被中斷地獲取鎖的特性:與synchronized不一樣,獲取到鎖的線程可以響應中斷,當獲取到鎖的線程被中斷時,中斷異常將會被拋出,同時鎖會被釋放;
超時獲取鎖的特性:在指定的時間範圍內獲取鎖;若是截止時間到了仍然沒法獲取鎖則返回。
從性能角度,synchronized 早期的實現比較低效,對比 ReentrantLock,大多數場景性能都相差較大。可是在 Java 6 中對其進行了很是多的改進,在高競爭狀況下,ReentrantLock 仍然有必定優點。在大多數狀況下,無需太糾結於性能,仍是考慮代碼書寫結構的便利性、可維護性等。
主要區別以下:
參考資料
《碼出高效:Java開發手冊》
Java核心技術36講:http://t.cn/EwUJvWA
Java中的鎖分類:https://www.cnblogs.com/qifengshi/p/6831055.html
課程推薦: