1. 程式人生 > >徹底搞懂synchronized(從偏向鎖到重量級鎖)

徹底搞懂synchronized(從偏向鎖到重量級鎖)

接觸過執行緒安全的同學想必都使用過synchronized這個關鍵字,在java同步程式碼快中,synchronized的使用方式無非有兩個:

  1. 通過對一個物件進行加鎖來實現同步,如下面程式碼。
synchronized(lockObject){
    //程式碼

}
  1. 對一個方法進行synchronized宣告,進而對一個方法進行加鎖來實現同步。如下面程式碼
public synchornized void test(){
    //程式碼
}

但這裡需要指出的是,無論是對一個物件進行加鎖還是對一個方法進行加鎖,實際上,都是對物件進行加鎖

也就是說,對於方式2,實際上虛擬機器會根據synchronized修飾的是例項方法還是類方法,去取對應的例項物件或者Class物件來進行加鎖。

對於synchronized這個關鍵字,可能之前大家有聽過,他是一個重量級鎖,開銷很大,建議大家少用點。但大家可能也聽說過,但到了jdk1.6之後,該關鍵字被進行了很多的優化,已經不像以前那樣不給力了,建議大家多使用。

那麼它是進行了什麼樣的優化,才使得synchronized又深得人心呢?為何重量級鎖開銷就大呢?

想必大家也都聽說過輕量級鎖,重量級鎖,自旋鎖,自適應自旋鎖,偏向鎖等等,他們都有哪些區別呢?
剛才和大家說,鎖是加在物件上的,那麼一個執行緒是如何知道這個物件被加了鎖呢?又是如何知道它加的是什麼型別的鎖呢?

基於這些問題,下面我講一步一步講解synchronized是如何被優化的,是如何從偏向鎖到重量級鎖的。

鎖物件

剛才我們說,鎖實際上是加在物件上的,那麼被加了鎖的物件我們稱之為鎖物件,在java中,任何一個物件都能成為鎖物件。
為了讓大家更好著理解虛擬機器是如何知道這個物件就是一個鎖物件的,我們下面簡單介紹一下java中一個物件的結構。
java物件在記憶體中的儲存結構主要有一下三個部分:

  1. 物件頭
  2. 例項資料
  3. 填充資料
    這裡強調一下,物件頭裡的資料主要是一些執行時的資料。
    其簡單的結構如下
長度 內容 說明
32/64bit Mark Work hashCode,GC分代年齡,鎖資訊
32/64bit Class Metadata Address 指向物件型別資料的指標
32/64bit Array Length 陣列的長度(當物件為陣列時)

從該表格中我們可以看到,物件中關於鎖的資訊是存在Markword裡的。
我們來看一段程式碼

LockObject lockObject = new LockObject();//隨便建立一個物件
synchronized(lockObject){
    //程式碼
}

當我們建立一個物件LockObject時,該物件的部分Markword關鍵資料如下。

bit fields 是否偏向鎖 鎖標誌位
hash 0 01

從圖中可以看出,偏向鎖的標誌位是“01”,狀態是“0”,表示該物件還沒有被加上偏向鎖。(“1”是表示被加上偏向鎖)。該物件被創建出來的那一刻,就有了偏向鎖的標誌位,這也說明了所有物件都是可偏向的,但所有物件的狀態都為“0”,也同時說明所有被建立的物件的偏向鎖並沒有生效。

偏向鎖

不過,當執行緒執行到臨界區(critical section)時,此時會利用CAS(Compare and Swap)操作,將執行緒ID插入到Markword中,同時修改偏向鎖的標誌位。

所謂臨界區,就是隻允許一個執行緒進去執行操作的區域,即同步程式碼塊。CAS是一個原子性操作

此時的Mark word的結構資訊如下:

bit fields 是否偏向鎖 鎖標誌位
threadId epoch 1 01

此時偏向鎖的狀態為“1”,說明物件的偏向鎖生效了,同時也可以看到,哪個執行緒獲得了該物件的鎖。

那麼,什麼是偏向鎖?

偏向鎖是jdk1.6引入的一項鎖優化,其中的“偏”是偏心的偏。它的意思就是說,這個鎖會偏向於第一個獲得它的執行緒,在接下來的執行過程中,假如該鎖沒有被其他執行緒所獲取,沒有其他執行緒來競爭該鎖,那麼持有偏向鎖的執行緒將永遠不需要進行同步操作。
也就是說:
在此執行緒之後的執行過程中,如果再次進入或者退出同一段同步塊程式碼,並不再需要去進行加鎖或者解鎖操作,而是會做以下的步驟:

  1. Load-and-test,也就是簡單判斷一下當前執行緒id是否與Markword當中的執行緒id是否一致.
  2. 如果一致,則說明此執行緒已經成功獲得了鎖,繼續執行下面的程式碼.
  3. 如果不一致,則要檢查一下物件是否還是可偏向,即“是否偏向鎖”標誌位的值。
  4. 如果還未偏向,則利用CAS操作來競爭鎖,也即是第一次獲取鎖時的操作。

如果此物件已經偏向了,並且不是偏向自己,則說明存在了競爭。此時可能就要根據另外執行緒的情況,可能是重新偏向,也有可能是做偏向撤銷,但大部分情況下就是升級成輕量級鎖了。
可以看出,偏向鎖是針對於一個執行緒而言的,執行緒獲得鎖之後就不會再有解鎖等操作了,這樣可以省略很多開銷。假如有兩個執行緒來競爭該鎖話,那麼偏向鎖就失效了,進而升級成輕量級鎖了。
為什麼要這樣做呢?因為經驗表明,其實大部分情況下,都會是同一個執行緒進入同一塊同步程式碼塊的。這也是為什麼會有偏向鎖出現的原因。
在Jdk1.6中,偏向鎖的開關是預設開啟的,適用於只有一個執行緒訪問同步塊的場景。

鎖膨脹

剛才說了,當出現有兩個執行緒來競爭鎖的話,那麼偏向鎖就失效了,此時鎖就會膨脹,升級為輕量級鎖。這也是我們經常所說的鎖膨脹

鎖撤銷

由於偏向鎖失效了,那麼接下來就得把該鎖撤銷,鎖撤銷的開銷花費還是挺大的,其大概的過程如下:

  1. 在一個安全點停止擁有鎖的執行緒。
  2. 遍歷執行緒棧,如果存在鎖記錄的話,需要修復鎖記錄和Markword,使其變成無鎖狀態。
  3. 喚醒當前執行緒,將當前鎖升級成輕量級鎖。
    所以,如果某些同步程式碼塊大多數情況下都是有兩個及以上的執行緒競爭的話,那麼偏向鎖就會是一種累贅,對於這種情況,我們可以一開始就把偏向鎖這個預設功能給關閉

輕量級鎖

鎖撤銷升級為輕量級鎖之後,那麼物件的Markword也會進行相應的的變化。下面先簡單描述下鎖撤銷之後,升級為輕量級鎖的過程:

  1. 執行緒在自己的棧楨中建立鎖記錄 LockRecord。
  2. 將鎖物件的物件頭中的MarkWord複製到執行緒的剛剛建立的鎖記錄中。
  3. 將鎖記錄中的Owner指標指向鎖物件。
  4. 將鎖物件的物件頭的MarkWord替換為指向鎖記錄的指標。

對應的圖描述如下(圖來自周志明深入java虛擬機器)
圖片1
圖片2

之後Markwork如下:

bit fields 鎖標誌位
指向LockRecord的指標 00

注:鎖標誌位”00”表示輕量級鎖
輕量級鎖主要有兩種

  1. 自旋鎖
  2. 自適應自旋鎖

自旋鎖

所謂自旋,就是指當有另外一個執行緒來競爭鎖時,這個執行緒會在原地迴圈等待,而不是把該執行緒給阻塞,直到那個獲得鎖的執行緒釋放鎖之後,這個執行緒就可以馬上獲得鎖的。
注意,鎖在原地迴圈的時候,是會消耗cpu的,就相當於在執行一個啥也沒有的for迴圈。
所以,輕量級鎖適用於那些同步程式碼塊執行的很快的場景,這樣,執行緒原地等待很短很短的時間就能夠獲得鎖了。
經驗表明,大部分同步程式碼塊執行的時間都是很短很短的,也正是基於這個原因,才有了輕量級鎖這麼個東西。

自旋鎖的一些問題

  1. 如果同步程式碼塊執行的很慢,需要消耗大量的時間,那麼這個時侯,其他執行緒在原地等待空消耗cpu,這會讓人很難受。
  2. 本來一個執行緒把鎖釋放之後,當前執行緒是能夠獲得鎖的,但是假如這個時候有好幾個執行緒都在競爭這個鎖的話,那麼有可能當前執行緒會獲取不到鎖,還得原地等待繼續空迴圈消耗cup,甚至有可能一直獲取不到鎖。

基於這個問題,我們必須給執行緒空迴圈設定一個次數,當執行緒超過了這個次數,我們就認為,繼續使用自旋鎖就不適合了,此時鎖會再次膨脹,升級為重量級鎖
預設情況下,自旋的次數為10次,使用者可以通過-XX:PreBlockSpin來進行更改。

自旋鎖是在JDK1.4.2的時候引入的

自適應自旋鎖

所謂自適應自旋鎖就是執行緒空迴圈等待的自旋次數並非是固定的,而是會動態著根據實際情況來改變自旋等待的次數。
其大概原理是這樣的:
假如一個執行緒1剛剛成功獲得一個鎖,當它把鎖釋放了之後,執行緒2獲得該鎖,並且執行緒2在執行的過程中,此時執行緒1又想來獲得該鎖了,但執行緒2還沒有釋放該鎖,所以執行緒1只能自旋等待,但是虛擬機器認為,由於執行緒1剛剛獲得過該鎖,那麼虛擬機器覺得執行緒1這次自旋也是很有可能能夠再次成功獲得該鎖的,所以會延長執行緒1自旋的次數
另外,如果對於某一個鎖,一個執行緒自旋之後,很少成功獲得該鎖,那麼以後這個執行緒要獲取該鎖時,是有可能直接忽略掉自旋過程,直接升級為重量級鎖的,以免空迴圈等待浪費資源。

輕量級鎖也被稱為非阻塞同步樂觀鎖,因為這個過程並沒有把執行緒阻塞掛起,而是讓執行緒空迴圈等待,序列執行。

重量級鎖

輕量級鎖膨脹之後,就升級為重量級鎖了。重量級鎖是依賴物件內部的monitor鎖來實現的,而monitor又依賴作業系統的MutexLock(互斥鎖)來實現的,所以重量級鎖也被成為互斥鎖
當輕量級所經過鎖撤銷等步驟升級為重量級鎖之後,它的Markword部分資料大體如下

bit fields 鎖標誌位
指向Mutex的指標 10

為什麼說重量級鎖開銷大呢

主要是,當系統檢查到鎖是重量級鎖之後,會把等待想要獲得鎖的執行緒進行阻塞,被阻塞的執行緒不會消耗cup。但是阻塞或者喚醒一個執行緒時,都需要作業系統來幫忙,這就需要從使用者態轉換到核心態,而轉換狀態是需要消耗很多時間的,有可能比使用者執行程式碼的時間還要長。
這就是說為什麼重量級執行緒開銷很大的。

互斥鎖(重量級鎖)也稱為阻塞同步悲觀鎖

總結

通過上面的分析,我們知道了為什麼synchronized關鍵字為何又深得人心,也知道了鎖的演變過程。
也就是說,synchronized關鍵字並非一開始就該物件加上重量級鎖,也是從偏向鎖,輕量級鎖,再到重量級鎖的過程。
這個過程也告訴我們,假如我們一開始就知道某個同步程式碼塊的競爭很激烈、很慢的話,那麼我們一開始就應該使用重量級鎖了,從而省掉一些鎖轉換的開銷。
講到這裡就大概完了,希望能對你有所幫助


參考資料

關注我的公眾號:苦逼的碼農,獲取更多原創文章,後臺回覆”禮包”送你一份特別的資源大禮包。

相關推薦

執行緒安全(上)--徹底synchronized(偏向重量級)

接觸過執行緒安全的同學想必都使用過synchronized這個關鍵字,在java同步程式碼快中,synchronized的使用方式無非有兩個: 通過對一個物件進行加鎖來實現同步,如下面程式碼。 synchronized(lockObject){  &nb

徹底synchronized(偏向重量級)

接觸過執行緒安全的同學想必都使用過synchronized這個關鍵字,在java同步程式碼快中,synchronized的使用方式無非有兩個: 通過對一個物件進行加鎖來實現同步,如下面程式碼。 synchronized(lockObject){ //程式碼 }

一道面試題徹底hashCode與equals的作用與區別及應當注意的細節

public class HashCodeTest { public static void main(String[] args) { Collection set = new HashSet(); Point p1 = new Point(1, 1); Point p2 = new Poin

java併發筆記之synchronized 偏向 輕量級 重量級證明

 警告⚠️:本文耗時很長,先做好心理準備 本篇將從hotspot原始碼(64 bits)入手,通過分析java物件頭引申出鎖的狀態;本文采用大量例項及分析,請耐心看完,謝謝   先來看一下hotspot的原始碼當中的物件頭的註釋(32bits 可以忽略了,現在基本沒有32位作業系

原始碼的角度徹底 HandlerMapping 和 HandlerAdapter

徹底搞懂 HandlerMapping和HandlerAdapter 知識點的回顧: 當Tomcat接收到請求後會回撥Servlet的service方法,一開始入門Servlet時,我們會讓自己的Servlet去實現HttpServlet介面,重寫它的doGet()和doPost()方法 在SpringM

徹底oracle的標量子查詢

article pop acc 問題 content 狀態 cat tracking varchar2 oracle標量子查詢和自己定義函數有時用起來比較方便,並且開發者也常常使用。數據量小還無所謂。數據量大,往往存在性能問題。 下面測試幫助大家徹底搞懂標量子查

看完讓你徹底Websocket原理

找到 說了 成了 原理 兩層 cep 告訴 edi 純粹 偶然在知乎上看到一篇回帖,瞬間覺得之前看的那麽多資料都不及這一篇回帖讓我對 websocket 的認識深刻有木有。所以轉到我博客裏,分享一下。比較喜歡看這種博客,讀起來很輕松,不枯燥,沒有布道師的陣仗,純粹為分享。廢

徹底反斜杠“”和正斜杠"/"的區別

影響 使用 web應用 圖片 命令 mic ont http () 正斜杠,符號是"/";反斜杠,符號是"\"。 在知乎中看到一個答案如下: 知乎用戶:“在絕大多數地方,用的都是/(slash),包括Mac/Linux,也包括URL。你唯一需要記住的是,Microsoft這

徹底Python的字符編碼

如果 標點符號 decode 編號 磁盤 性能 用處 效果 必須 前言:中文編碼問題一直是程序員頭疼的問題,而Python2中的字符編碼足矣令新手抓狂。本文將盡量用通俗的語言帶大家徹底的了解字符編碼以及Python2和3中的各種編碼問題。 一、什麽是字符編碼。 要徹底解決字

徹底 Python 編碼

腳本文件 syntax 文件編碼 一次 sci tail 關聯 習慣 class 因為中文的特殊編碼,導致 Python2 和 Python3 使用過程中的各種編碼問題,如果不清楚其中的關聯關系,那麽這就一直是個大坑,不是懵逼就還是懵逼,所以就目前碰到的情況徹底梳理下 Py

轉--看完讓你徹底Websocket原理

接下來 lur 耗資源 最終 ive img pro -- 傳遞 偶然在知乎上看到一篇回帖,瞬間覺得之前看的那麽多資料都不及這一篇回帖讓我對 websocket 的認識深刻有木有。所以轉到我博客裏,分享一下。比較喜歡看這種博客,讀起來很輕松,不枯燥,沒有布道師的陣仗,純粹為

聽說看了這篇文章就徹底了什麽是OPC(上)

文檔 files 兩個 tool 共存 硬件 信息 更改 消息 從2000年初以來,我們就一直在使用OPC軟件互操作性標準,而那些正準備踏入和想要踏入工業自動化領域的人們卻對這些含義感到困惑。 所以在本中,我將系統地為你梳理OPC知識。 OPC首字母縮寫詞代表什麽? 問一

Java偏向\輕量級\重量級總結

  資源消耗 目的 場景 實現方式 偏向鎖 一個執行緒只有一次CAS 單執行緒進行同步塊時,消除輕量級鎖的CAS操作。 大多數場景為單執

一文徹底python中的self

在介紹Python的self用法之前,先來介紹下Python中的類和例項……  我們知道,面向物件最重要的概念就是類(class)和例項(instance),類是抽象的模板,比如學生這個抽象的事物,可以用一個Student類來表示。而例項是根據類創建出來的一個個具體的“物件”,每一個物件都

徹底狀態機(一段式、兩段式、三段式)

例項:FSM實現10010串的檢測 狀態轉移圖:初始狀態S0,a = 0,z = 0.如果檢測到1,跳轉到S1。             下一狀態S1,a = 1,z = 0.如果檢測到0,跳轉到S2。 &nb

websocket(轉) 看完讓你徹底Websocket原理

看完讓你徹底搞懂Websocket原理 偶然在知乎上看到一篇回帖,瞬間覺得之前看的那麼多資料都不及這一篇回帖讓我對 websocket 的認識深刻有木有。所以轉到我部落格裡,分享一下。比較喜歡看這種部落格,讀起來很輕鬆,不枯燥,沒有佈道師的陣仗,純粹為分享。廢話這麼多了,最後再贊一

徹底Java的值傳遞和引用傳遞

學過Java基礎的人都知道:值傳遞和引用傳遞是初次接觸Java時的一個難點,有時候記得了語法卻記不得怎麼實際運用,有時候會的了運用卻解釋不出原理,而且坊間討論的話題又是充滿爭議:有的論壇帖子說Java只有值傳遞,有的部落格說兩者皆有;這讓人有點摸不著頭腦,下面我們就這個話題做一些探討,對書籍、對論壇

一文徹底python的垃圾回收機制

  一 、什麼是記憶體管理和垃圾回收 Python GC主要使用引用計數(reference counting)來跟蹤和回收垃圾。在引用計數的基礎上,通過“標記-清除”(mark and sweep)解決容器物件可能產生的迴圈引用問題,通過“分代回收”(genera

一文徹底卷積神經網路的“感受野”,看不你來找我!

  一、什麼是“感受野” 1.1 感受野的概念 “感受野”的概念來源於生物神經科學,比如當我們的“感受器”,比如我們的手受到刺激之後,會將刺激傳輸至中樞神經,但是並不是一個神經元就能夠接受整個面板的刺激,因為面板面積大,一個神經元可想而知肯定接受不完,而且我們同

徹底錯排公式

問題:現有10本書按照順序擺放,現要求重新排列,使得新的書的順序中每一本書都不在原來的位置,求有多少種排列方式? 這個問題推廣一下,就是錯排問題,是組合數學中的問題之一。考慮一個有n個元素的排列,若一個排列中所有的元素都不在自己原來的位置上,那麼這樣的排列就稱為原排列的一個錯排。 n個元素的