大話Synchronized及鎖升級

前言

小夥伴你們好,我是jack xu,今天是清明假期,跟你們來聊一聊synchronized。本篇是併發編程中的第一篇,爲何說是第一篇呢,由於併發編程涉及的東西太多太多,晦澀難懂,隨便一個知識點拉出來均可以寫一篇文章,如此算來寫完併發編程一個系列最起碼要十篇。我將知識點進行了總結概括,排類分類,用通俗易懂的方式來跟你們說清楚、講明白。。java

爲何要用Synchronized

這個問題很簡單,首先咱們來看下面這個代碼linux

開10000個線程,將變量count遞增,結果是9998,很顯然是出現了線程不安全。那爲何會出現這樣的結果呢,答案也很簡單

這裏稍微解釋下爲啥會得不到 100(知道的可直接跳過), i++ 這個操做,計算機須要分紅三步來執行。
一、讀取 i 的值。
二、把 i 加 1.
三、把 最終 i 的結果寫入內存之中。
因此,(1)、假如線程 A 讀取了 i 的值爲 i = 0,(2)、這個時候線程 B 也讀取了 i 的值 i = 0。
(3)、接着 A把 i 加 1,而後寫入內存,此時 i = 1。(4)、緊接着,B也把 i 加 1,此時線程B中的 i = 1,
而後線程B 把 i 寫入內存,此時內存中的 i = 1。也就是說,線程 A, B 都對 i 進行了自增,但最終的結果倒是1,不是 2.
複製代碼

歸根到底一句話就是這麼多操做不是原子性,那怎麼解決這個問題呢,加上Synchronized便可編程

三大特性

在上面例子演示的是原子性。synchronized 能夠確保可見性,根據happens-before規定,在一個線程執行完 synchronized 代碼後,全部代碼中對變量值的變化都能當即被其它線程所看到。順序性的話就是禁止指令重排,代碼塊中的代碼從上往下依次執行,歸根到底再一句話,併發問題中的三個特性synchronized都能保證,也就是synchronized是萬金油,用他準沒錯!安全

使用方法

從語法上講,Synchronized總共有三種用法:bash

  • 修飾實例方法
public synchronized void eat(){
	.......
  .......
}
複製代碼
  • 修飾靜態方法
public static synchronized void eat(){
	.......
  .......
}
複製代碼
  • 修飾代碼塊
public void eat(){
   synchronized(this){
   	.......
 	.......
   }
}
複製代碼
public void eat(){
   synchronized(Eat.class){
   	.......
 	.......
   }
}
複製代碼

其中第一種和第三種對等,第二種和第四種對等,這個很簡單,下面是使用 synchronized的總結:markdown

  • 選用一個鎖對象,能夠是任意對象;
  • 鎖對象鎖的是同步代碼塊,並非本身;
  • 不一樣類型的多個 Thread 若是有代碼要同步執行,鎖對象要使用全部線程共同持有的同一個對象;
  • 須要同步的代碼放到大括號中。須要同步的意思就是須要保證原子性、可見性、有序性中的任何一種或多種。不要放不須要同步的代碼進來,影響代碼效率。

鎖升級

好,本文的高潮來了,你們仔細聽,在JDK的早期,synchronized叫作重量級鎖,由於申請鎖資源必須經過kernel,系統調用,從用戶態 -> 內核態的轉換,效率比較低,JDK1.6 以後作了一些優化,爲了減小得到鎖和釋放鎖帶來的性能開銷,引入了偏向鎖、輕量級鎖的概念。所以你們會發如今 synchronized 中,鎖存在四種狀態分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖;多線程

咱們知道synchronized鎖的是對象,對象就是Object,Object在heap中的佈局,以下圖所示併發

前面8個字節就是markword,後面4個字節是class pointer就是這個對象屬於哪一個類的,People就是People.class,Cat類就是Cat.class,在後面實例數據就是看你類裏面字段的具體大小了,int age就是4個字節,string name就是英文1個字節, 中文2個字節(String的中文字節數要看用的編碼集合,若是是utf-8類型的,那麼中文佔2到3個字節,若是是GBK類型的,那麼中文佔2個字節),最後前面三項加起來不能被8整除的,就是補齊到可以被8整除。下圖就是markword(8*8=64位)的分佈圖,鎖升級就是markdown裏面標誌位的變化。

網上因此的圖都是32位的,我這裏畫的是64位的,你們發現一共有五種狀態,用兩位是不夠的,因此01的時候在向前借一位。

偏向鎖

hotspot虛擬機的做者通過調查發現,大部分狀況下,加鎖的代碼不只僅不存在多線程競爭,並且老是由同一個線程屢次得到。因此基於這樣一個機率,咱們一開始加鎖上的是偏向鎖,當一個線程訪問加了同步鎖的代碼塊時,首先會嘗試經過CAS操做在對象頭中存儲當前線程的ID

(1)若是成功markword則存儲當前線程ID,接着執行同步代碼塊app

(2)若是是同一個線程加鎖的時候,不須要爭用,只須要判斷線程指針是否同一個,可直接執行同步代碼塊jvm

(3)若是有其餘線程已經得到了偏向鎖,這種狀況說明當前鎖存在競爭,須要撤銷已得到偏向鎖的線程,而且把它持有的鎖升級爲輕量級鎖(這個操做須要等到全局安全點,也就是沒有線程在執行字節碼)才能執行

在咱們的應用開發中,絕大部分狀況下必定會存在 2 個以上的線程競爭,那麼若是開啓偏向鎖,反而會提高獲取鎖的資源消耗。因此能夠經過jvm參數UseBiasedLocking 來設置開啓或關閉偏向鎖

輕量級鎖

撤銷偏向鎖,升級輕量級鎖,每一個線程在本身的線程棧生成LockRecord,用CAS操做將markword設置爲指向本身這個線程的LR的指針,設置成功者獲得鎖。 輕量級鎖在加鎖過程當中,用到了自旋鎖,自旋鎖的使用,其實也是有必定條件的,若是一個線程執行同步代碼塊的時間很長,那麼這個線程不斷的循環反而會消耗 CPU 資源。

(1)默認狀況下自旋的次數是 10 次,能夠經過-XX:PreBlockSpin來修改,或者自旋線程數超過CPU核數的一半

(2)在 JDK1.6 以後,引入了自適應自旋鎖,自適應意味着自旋的次數不是固定不變的,而是根據前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態來決定。若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也是頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間。若是對於某個鎖,自旋不多成功得到過,那在之後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源

知足這兩種狀況之一後升級爲重量級鎖

重量級鎖

這時候就驚動老佛爺了,向操做系統申請資源,linux mutex , CPU從3級-0級系統調用,線程掛起,進入等待隊列,等待操做系統的調度,而後再映射回用戶空間。

咱們隨便寫一段簡單的帶有 synchronized 關鍵字的代碼。先將其編譯爲.class 文件,而後使用 javap -c xxx.class 進行反彙編。咱們就能夠獲得 java 代碼對應的彙編指令。裏面能夠找到以下兩行指令。

字節碼層面就是關鍵的這兩條指令,monitorenter,moniterexit  (注:代碼塊用的是ACC_SYNCHRONIZED,這是一個標誌位,底層原理仍是這兩條指令)

java中每一個對象都關聯了一個監視器鎖monitor,當monitor被佔用時就會處於鎖定狀態。線程執行monitorenter 指令時嘗試獲取monitor的全部權,過程以下:

  • 若是monitor的進入數爲 0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor 的全部者。
  • 若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加 1。
  • 若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲 0,再從新嘗試獲取monitor的全部權。

從上面過程能夠看出兩點,第一:monitor是可重入的,他有計數器,第二:monitor是非公平鎖

monitor 依賴操做系統的mutexLock(互斥鎖)來實現的,線程被阻塞後便進入內核(Linux)調度狀態,這個會致使系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能

鎖消除

咱們都知道 StringBuffer 是線程安全的,由於它的關鍵方法都是被 synchronized修飾過的,但咱們看上面這段代碼,咱們會發現,sb 這個引用只會在 add方法中使用,不可能被其它線程引用(由於是局部變量,棧私有),所以 sb是不可能共享的資源,JVM 會自動消除 StringBuffer 對象內部的鎖。

public void add(String str1,String str2){
         StringBuffer sb = new StringBuffer();
         sb.append(str1).append(str2);
}
複製代碼

總結

好,本文對synchronized所涵蓋的知識點已經講解的很清楚了。synchronized是Java併發編程中最經常使用的用於保證線程安全的方式,其使用相對也比較簡單。在synchronized優化之前,synchronized的性能是比ReentrantLock差不少的,可是自從synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,二者的性能就差很少了。 在兩種方法均可用的狀況下,官方甚至建議使用synchronized,其實synchronized的優化我感受就借鑑了ReentrantLock中的CAS技術。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。

相關文章
相關標籤/搜索