前言
小夥伴大家好,我是jack xu,今天是清明假期,跟大家來聊一聊synchronized。本篇是併發程式設計中的第一篇,為什麼說是第一篇呢,因為併發程式設計涉及的東西太多太多,晦澀難懂,隨便一個知識點拉出來都可以寫一篇文章,如此算來寫完併發程式設計一個系列最起碼要十篇。我將知識點進行了總結歸納,排類分類,用通俗易懂的方式來跟大家說清楚、講明白。。
為什麼要用Synchronized
這個問題很簡單,首先我們來看下面這個程式碼

這裡稍微解釋下為啥會得不到 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 可以確保可見性,根據happens-before規定,在一個執行緒執行完 synchronized 程式碼後,所有程式碼中對變數值的變化都能立即被其它執行緒所看到。順序性的話就是禁止指令重排,程式碼塊中的程式碼從上往下依次執行,歸根到底再一句話,併發問題中的三個特性synchronized都能保證,也就是synchronized是萬金油,用他準沒錯!
使用方法
從語法上講,Synchronized總共有三種用法:
- 修飾例項方法
public synchronized void eat(){
.......
.......
}
複製程式碼
- 修飾靜態方法
public static synchronized void eat(){
.......
.......
}
複製程式碼
- 修飾程式碼塊
public void eat(){
synchronized(this){
.......
.......
}
}
複製程式碼
public void eat(){
synchronized(Eat.class){
.......
.......
}
}
複製程式碼
其中第一種和第三種對等,第二種和第四種對等,這個很簡單,下面是使用 synchronized的總結:
- 選用一個鎖物件,可以是任意物件;
- 鎖物件鎖的是同步程式碼塊,並不是自己;
- 不同型別的多個 Thread 如果有程式碼要同步執行,鎖物件要使用所有執行緒共同持有的同一個物件;
- 需要同步的程式碼放到大括號中。需要同步的意思就是需要保證原子性、可見性、有序性中的任何一種或多種。不要放不需要同步的程式碼進來,影響程式碼效率。
鎖升級
好,本文的高潮來了,大家仔細聽,在JDK的早期,synchronized叫做重量級鎖,因為申請鎖資源必須通過kernel,系統呼叫,從使用者態 -> 核心態的轉換,效率比較低,JDK1.6 之後做了一些優化,為了減少獲得鎖和釋放鎖帶來的效能開銷,引入了偏向鎖、輕量級鎖的概念。因此大家會發現在 synchronized 中,鎖存在四種狀態分別是:無鎖、偏向鎖、輕量級鎖、重量級鎖;
我們知道synchronized鎖的是物件,物件就是Object,Object在heap中的佈局,如下圖所示


偏向鎖

(1)如果成功markword則儲存當前執行緒ID,接著執行同步程式碼塊
(2)如果是同一個執行緒加鎖的時候,不需要爭用,只需要判斷執行緒指標是否同一個,可直接執行同步程式碼塊
(3)如果有其他執行緒已經獲得了偏向鎖,這種情況說明當前鎖存在競爭,需要撤銷已獲得偏向鎖的執行緒,並且把它持有的鎖升級為輕量級鎖(這個操作需要等到全域性安全點,也就是沒有執行緒在執行位元組碼)才能執行
在我們的應用開發中,絕大部分情況下一定會存在 2 個以上的執行緒競爭,那麼如果開啟偏向鎖,反而會提升獲取鎖的資源消耗。所以可以通過jvm引數UseBiasedLocking 來設定開啟或關閉偏向鎖
輕量級鎖

(1)預設情況下自旋的次數是 10 次,可以通過-XX:PreBlockSpin來修改,或者自旋執行緒數超過CPU核數的一半
(2)在 JDK1.6 之後,引入了自適應自旋鎖,自適應意味著自旋的次數不是固定不變的,而是根據前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態來決定。如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒正在執行中,那麼虛擬機器就會認為這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞執行緒,避免浪費處理器資源
滿足這兩種情況之一後升級為重量級鎖
重量級鎖
這時候就驚動老佛爺了,向作業系統申請資源,linux mutex , CPU從3級-0級系統呼叫,執行緒掛起,進入等待佇列,等待作業系統的排程,然後再對映回使用者空間。
我們隨便寫一段簡單的帶有 synchronized 關鍵字的程式碼。先將其編譯為.class 檔案,然後使用 javap -c xxx.class 進行反彙編。我們就可以得到 java 程式碼對應的彙編指令。裡面可以找到如下兩行指令。

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技術。都是試圖在使用者態就把加鎖問題解決,避免進入核心態的執行緒阻塞。