大話Synchronized及鎖升級

作者:jack_xu

juejin.im/post/5e898b8fe51d4546cd2fda30

為什麼要用Synchronized

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

大話Synchronized及鎖升級

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

這裡稍微解釋下為啥會得不到 100(知道的可直接跳過), i++ 這個操作,計算機需要分成三步來執行。

  1. 讀取 i 的值。
  2. 把 i 加 1.
  3. 把 最終 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及鎖升級

三大特性

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

使用方法

<code>從語法上講,Synchronized總共有三種用法:/<code>

修飾實例方法

<code>public synchronized void eat(){
.......
.......
}/<code>

修飾靜態方法

<code>public static synchronized void eat(){
.......
.......
}/<code>

修飾代碼塊

<code>public void eat(){
synchronized(this){
.......
.......
}
}/<code>
<code>public void eat(){
synchronized(Eat.class){
.......
.......
}
}/<code>

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

  • 選用一個鎖對象,可以是任意對象;
  • 鎖對象鎖的是同步代碼塊,並不是自己;
  • 不同類型的多個 Thread 如果有代碼要同步執行,鎖對象要使用所有線程共同持有的同一個對象;
  • 需要同步的代碼放到大括號中。需要同步的意思就是需要保證原子性、可見性、有序性中的任何一種或多種。不要放不需要同步的代碼進來,影響代碼效率。

鎖升級

好,本文的高潮來了,大家仔細聽,在JDK的早期,synchronized叫做重量級鎖,因為申請鎖資源必須通過kernel,系統調用,從用戶態 -> 內核態的轉換,效率比較低,JDK1.6 之後做了一些優化,為了減少獲得鎖和釋放鎖帶來的性能開銷,引入了偏向鎖、輕量級鎖的概念。因此大家會發現在 synchronized 中,鎖存在四種狀態分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖;

我們知道synchronized鎖的是對象,對象就是Object,Object在heap中的佈局,如下圖所示

大話Synchronized及鎖升級

前面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裡面標誌位的變化。

大話Synchronized及鎖升級

網上所以的圖都是32位的,我這裡畫的是64位的,大家發現一共有五種狀態,用兩位是不夠的,所以01的時候在向前借一位。

偏向鎖

大話Synchronized及鎖升級

hotspot虛擬機的作者經過調查發現,大部分情況下,加鎖的代碼不僅僅不存在多線程競爭,而且總是由同一個線程多次獲得。所以基於這樣一個概率,我們一開始加鎖上的是偏向鎖,當一個線程訪問加了同步鎖的代碼塊時,首先會嘗試通過CAS操作在對象頭中存儲當前線程的ID

  • 如果成功markword則存儲當前線程ID,接著執行同步代碼塊
  • 如果是同一個線程加鎖的時候,不需要爭用,只需要判斷線程指針是否同一個,可直接執行同步代碼塊
  • 如果有其他線程已經獲得了偏向鎖,這種情況說明當前鎖存在競爭,需要撤銷已獲得偏向鎖的線程,並且把它持有的鎖升級為輕量級鎖(這個操作需要等到全局安全點,也就是沒有線程在執行字節碼)才能執行

在我們的應用開發中,絕大部分情況下一定會存在 2 個以上的線程競爭,那麼如果開啟偏向鎖,反而會提升獲取鎖的資源消耗。所以可以通過jvm參數UseBiasedLocking 來設置開啟或關閉偏向鎖。偏向鎖詳細:死磕Synchronized底層實現--偏向鎖

輕量級鎖

大話Synchronized及鎖升級

撤銷偏向鎖,升級輕量級鎖,每個線程在自己的線程棧生成LockRecord,用CAS操作將markword設置為指向自己這個線程的LR的指針,設置成功者得到鎖。輕量級鎖在加鎖過程中,用到了自旋鎖,自旋鎖的使用,其實也是有一定條件的,如果一個線程執行同步代碼塊的時間很長,那麼這個線程不斷的循環反而會消耗 CPU 資源。輕量級詳細:死磕Synchronized底層實現--輕量級鎖

  • 默認情況下自旋的次數是 10 次,可以通過-XX:PreBlockSpin來修改,或者自旋線程數超過CPU核數的一半
  • 在 JDK1.6 之後,引入了自適應自旋鎖,自適應意味著自旋的次數不是固定不變的,而是根據前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認為這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源

滿足這兩種情況之一後升級為重量級鎖

重量級鎖

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

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

大話Synchronized及鎖升級

字節碼層面就是關鍵的這兩條指令,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)調度狀態,這個會導致系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能。重量級鎖詳細:死磕Synchronized底層實現--重量級鎖

鎖消除

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

<code>public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}/<code>

總結

好,本文對synchronized所涵蓋的知識點已經講解的很清楚了。synchronized是Java併發編程中最常用的用於保證線程安全的方式,其使用相對也比較簡單。在synchronized優化以前,synchronized的性能是比ReentrantLock差很多的,但是自從synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,兩者的性能就差不多了。

在兩種方法都可用的情況下,官方甚至建議使用synchronized,其實synchronized的優化我感覺就借鑑了ReentrantLock中的CAS技術。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。

對了,在這裡說一下,我目前是在職Java開發,如果你現在正在學習Java,瞭解Java,渴望成為一名合格的Java開發工程師,在入門學習Java的過程當中缺乏基礎入門的視頻教程,可以關注並私信我:01。獲取。我這裡有最新的Java基礎全套視頻教程。


分享到:


相關文章: