天天看點

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

一、為什麼要用鎖?

鎖-是為了解決并發操作引起的髒讀、資料不一緻的問題。

二、鎖實作的基本原理

2.1、volatile

Java程式設計語言允許線程通路共享變量, 為了確定共享變量能被準确和一緻地更新,線程應該確定通過排他鎖單獨獲得這個變量。Java語言提供了volatile,在某些情況下比鎖要更加友善。

volatile在多處理器開發中保證了共享變量的“ 可見性”。可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

結論:如果volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起線程上下文的切換和排程。

2.2、synchronized

synchronized通過鎖機制實作同步。

先來看下利用synchronized實作同步的基礎:Java中的每一個對象都可以作為鎖。

具體表現為以下3種形式。

  • 對于普通同步方法,鎖是目前執行個體對象。
  • 對于靜态同步方法,鎖是目前類的Class對象。
  • 對于同步方法塊,鎖是Synchonized括号裡配置的對象。

當一個線程試圖通路同步代碼塊時,它首先必須得到鎖,退出或抛出異常時必須釋放鎖。

2.2.1 synchronized實作原理

synchronized是基于Monitor來實作同步的。

Monitor從兩個方面來支援線程之間的同步:

  • 互斥執行
  • 協作

1、Java 使用對象鎖 ( 使用 synchronized 獲得對象鎖 ) 保證工作在共享的資料集上的線程互斥執行。

2、使用 notify/notifyAll/wait 方法來協同不同線程之間的工作。

3、Class和Object都關聯了一個Monitor。

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

Monitor 的工作機理

  • 線程進入同步方法中。
  • 為了繼續執行臨界區代碼,線程必須擷取 Monitor 鎖。如果擷取鎖成功,将成為該監視者對象的擁有者。任一時刻内,監視者對象隻屬于一個活動線程(The Owner)
  • 擁有監視者對象的線程可以調用 wait() 進入等待集合(Wait Set),同時釋放監視鎖,進入等待狀态。
  • 其他線程調用 notify() / notifyAll() 接口喚醒等待集合中的線程,這些等待的線程需要重新擷取監視鎖後才能執行 wait() 之後的代碼。
  • 同步方法執行完畢了,線程退出臨界區,并釋放監視鎖。

參考文檔:https://www.ibm.com/developerworks/cn/java/j-lo-synchronized

2.2.2 synchronized具體實作

1、同步代碼塊采用monitorenter、monitorexit指令顯式的實作。

2、同步方法則使用ACC_SYNCHRONIZED标記符隐式的實作。

通過執行個體來看看具體實作:

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

javap編譯後的位元組碼如下:

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

image

monitorenter

每一個對象都有一個monitor,一個monitor隻能被一個線程擁有。當一個線程執行到monitorenter指令時會嘗試擷取相應對象的monitor,擷取規則如下:

  • 如果monitor的進入數為0,則該線程可以進入monitor,并将monitor進入數設定為1,該線程即為monitor的擁有者。
  • 如果目前線程已經擁有該monitor,隻是重新進入,則進入monitor的進入數加1,是以synchronized關鍵字實作的鎖是可重入的鎖。
  • 如果monitor已被其他線程擁有,則目前線程進入阻塞狀态,直到monitor的進入數為0,再重新嘗試擷取monitor。

monitorexit

隻有擁有相應對象的monitor的線程才能執行monitorexit指令。每執行一次該指令monitor進入數減1,當進入數為0時目前線程釋放monitor,此時其他阻塞的線程将可以嘗試擷取該monitor。

2.2.3 鎖存放的位置

鎖标記存放在Java對象頭的Mark Word中。

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

Java對象頭長度

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

32位JVM Mark Word 結構

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

32位JVM Mark Word 狀态變化

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

64位JVM Mark Word 結構

2.2.3 synchronized的鎖優化

JavaSE1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”。

在JavaSE1.6中,鎖一共有4種狀态,級别從低到高依次是:無鎖狀态、偏向鎖狀态、輕量級鎖狀态和重量級鎖狀态,這幾個狀态會随着競争情況逐漸更新。

鎖可以更新但不能降級,意味着偏向鎖更新成輕量級鎖後不能降級成偏向鎖。這種鎖更新卻不能降級的政策,目的是為了提高獲得鎖和釋放鎖的效率。

偏向鎖:

無鎖競争的情況下為了減少鎖競争的資源開銷,引入偏向鎖。

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

輕量級鎖:

輕量級鎖所适應的場景是線程交替執行同步塊的情況。

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

鎖粗化(Lock Coarsening):也就是減少不必要的緊連在一起的unlock,lock操作,将多個連續的鎖擴充成一個範圍更大的鎖。

鎖消除(Lock Elimination):鎖削除是指虛拟機即時編譯器在運作時,對一些代碼上要求同步,但是被檢測到不可能存在共享資料競争的鎖進行削除。

适應性自旋(Adaptive Spinning):自适應意味着自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀态來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運作中,那麼虛拟機就會認為這次自旋也很有可能再次成功,進而它将允許自旋等待持續相對更長的時間,比如100個循環。另一方面,如果對于某個鎖,自旋很少成功獲得過,那在以後要擷取這個鎖時将可能省略掉自旋過程,以避免浪費處理器資源。

2.2.4 鎖的優缺點對比

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

2.3、CAS

CAS,在Java并發應用中通常指CompareAndSwap或CompareAndSet,即比較并交換。

1、CAS是一個原子操作,它比較一個記憶體位置的值并且隻有相等時修改這個記憶體位置的值為新的值,保證了新的值總是基于最新的資訊計算的,如果有其他線程在這期間修改了這個值則CAS失敗。CAS傳回是否成功或者記憶體位置原來的值用于判斷是否CAS成功。

2、JVM中的CAS操作是利用了處理器提供的CMPXCHG指令實作的。

優點:

  • 競争不大的時候系統開銷小。

缺點:

  • 循環時間長開銷大。
  • ABA問題。
  • 隻能保證一個共享變量的原子操作。

三、Java中的鎖實作

3.1、隊列同步器(AQS)

隊列同步器AbstractQueuedSynchronizer(以下簡稱同步器),是用來建構鎖或者其他同步元件的基礎架構。

3.1.1、它使用了一個int成員變量表示同步狀态。

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

3.1.2、通過内置的FIFO雙向隊列來完成擷取鎖線程的排隊工作。

  • 同步器包含兩個節點類型的應用,一個指向頭節點,一個指向尾節點,未擷取到鎖的線程會建立節點線程安全(compareAndSetTail)的加入隊列尾部。同步隊列遵循FIFO,首節點是擷取同步狀态成功的節點。
Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例
  • 未擷取到鎖的線程将建立一個節點,設定到尾節點。如下圖所示:
Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例
  • 首節點的線程在釋放鎖時,将會喚醒後繼節點。而後繼節點将會在擷取鎖成功時将自己設定為首節點。如下圖所示:
Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

3.1.3、獨占式/共享式鎖擷取

獨占式:有且隻有一個線程能擷取到鎖,如:ReentrantLock;

共享式:可以多個線程同時擷取到鎖,如:CountDownLatch;

獨占式

  • 每個節點自旋觀察自己的前一節點是不是Header節點,如果是,就去嘗試擷取鎖。
Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例
  • 獨占式鎖擷取流程:
Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

共享式:

  • 共享式與獨占式的差別:
Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例
  • 共享鎖擷取流程:
Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

四、鎖的使用用例

4.1、ConcurrentHashMap的實作原理及使用

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

ConcurrentHashMap類圖

Java中的鎖原理、鎖優化、CAS、AQS詳解一、為什麼要用鎖?二、鎖實作的基本原理三、Java中的鎖實作四、鎖的使用用例

ConcurrentHashMap資料結構

結論:ConcurrentHashMap使用的鎖分段技術。首先将資料分成一段一段地存儲,然後給每一段資料配一把鎖,當一個線程占用鎖通路其中一個段資料的時候,其他段的資料也能被其他線程通路。

群内提供免費的Java架構學習資料,QQ群:643459718

(裡面有高可用、高并發、高性能及分布式、Jvm性能調優、Spring源碼,

MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)