http://www.infoq.com/cn/articles/java-se-16-synchronized
[TOC]
## 1 引言
在多線程并發編程中Synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖,但是隨著Java SE1.6對Synchronized進行了各種優化之后,有些情況下它并不那么重了,本文詳細介紹了Java SE1.6中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖的存儲結構和升級過程。
## 2 術語定義
| | | |
| --- | --- | --- |
| 術語 | 英文| 說明 |
| CAS | Compare and Swap |
比較并設置。用于在硬件層面上提供原子性操作。在?Intel?處理器中,比較并交換通過指令cmpxchg實現。比較是否和給定的數值一致,如果一致則修改,不一致則不修改。
|
## 3 同步的基礎
Java中的每一個對象都可以作為鎖。
* 對于同步方法,鎖是當前實例對象。
* 對于靜態同步方法,鎖是當前對象的Class對象。
* 對于同步方法塊,鎖是Synchonized括號里配置的對象。
當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。那么鎖存在哪里呢?鎖里面會存儲什么信息呢?
## 4 同步的原理
JVM規范規定JVM基于進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現,而方法同步是使用另外一種方式實現的,細節在JVM規范里并沒有詳細說明,但是方法的同步同樣可以使用這兩個指令來實現。monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處, JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個 monitor 與之關聯,當且一個monitor 被持有后,它將處于鎖定狀態。線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 monitor 的所有權,即嘗試獲得對象的鎖。
### 4.1 Java對象頭
鎖存在Java對象頭里。如果對象是數組類型,則虛擬機用3個Word(字寬)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,一字寬等于四字節,即32bit。
|
長度
|
內容
|
說明
|
|
32/64bit
|
Mark Word
|
存儲對象的hashCode或鎖信息等。
|
|
32/64bit
|
Class Metadata Address
|
存儲到對象類型數據的指針
|
|
32/64bit
|
Array length
|
數組的長度(如果當前對象是數組)
|
Java對象頭里的Mark Word里默認存儲對象的HashCode,分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構如下:
|
?
|
25 bit
|
4bit
|
1bit
是否是偏向鎖
|
2bit
鎖標志位
|
|
無鎖狀態
|
對象的hashCode
|
對象分代年齡
|
0
|
01
|
在運行期間Mark Word里存儲的數據會隨著鎖標志位的變化而變化。Mark Word可能變化為存儲以下4種數據:
|
鎖狀態
|
25 bit
|
4bit
|
1bit
|
2bit
|
|
23bit
|
2bit
|
是否是偏向鎖
|
鎖標志位
|
|
輕量級鎖
|
指向棧中鎖記錄的指針
|
00
|
|
重量級鎖
|
指向互斥量(重量級鎖)的指針
|
10
|
|
GC標記
|
空
|
11
|
|
偏向鎖
|
線程ID
|
Epoch
|
對象分代年齡
|
1
|
01
|
在64位虛擬機下,Mark Word是64bit大小的,其存儲結構如下:?
|
鎖狀態
|
25bit
|
31bit
|
1bit
|
4bit
|
1bit
|
2bit
|
|
?
|
?
|
cms_free
|
分代年齡
|
偏向鎖
|
鎖標志位
|
|
無鎖
|
unused
|
hashCode
|
?
|
?
|
0
|
01
|
|
偏向鎖
|
ThreadID(54bit) Epoch(2bit)
|
?
|
?
|
1
|
01
|
### 4.2 鎖的升級
Java SE1.6為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6里鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率,下文會詳細分析。

### 4.3 偏向鎖
Hotspot的作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖,如果測試成功,表示線程已經獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖),如果沒有設置,則使用CAS競爭鎖,如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態,則將對象頭設置成無鎖狀態,如果線程仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖,最后喚醒暫停的線程。下圖中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。

關閉偏向鎖:偏向鎖在Java 6和Java 7里是默認啟用的,但是它在應用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數來關閉延遲-XX:BiasedLockingStartupDelay = 0。如果你確定自己應用程序里所有的鎖通常情況下處于競爭狀態,可以通過JVM參數關閉偏向鎖-XX:-UseBiasedLocking=false,那么默認會進入輕量級鎖狀態。
### 4.4 輕量級鎖
輕量級鎖加鎖:線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
輕量級鎖解鎖:輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖是兩個線程同時爭奪鎖,導致鎖膨脹的流程圖。

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處于這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。
## 5 鎖的優缺點對比
|
鎖
|
優點
|
缺點
|
適用場景
|
|
偏向鎖
|
加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。
|
如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。
|
適用于只有一個線程訪問同步塊場景。
|
|
輕量級鎖
|
競爭的線程不會阻塞,提高了程序的響應速度。
|
如果始終得不到鎖競爭的線程使用自旋會消耗CPU。
|
追求響應時間。
同步塊執行速度非常快。
|
|
重量級鎖
|
線程競爭不使用自旋,不會消耗CPU。
|
線程阻塞,響應時間緩慢。
|
追求吞吐量。
同步塊執行速度較長。
|
## 6 參考源碼
本文一些內容參考了[HotSpot](http://hg.openjdk.java.net/hsx/hotspot-main/hotspot/file/61b82be3b1ff/)源碼 。對象頭源碼markOop.hpp。偏向鎖源碼biasedLocking.cpp。以及其他源碼ObjectMonitor.cpp和BasicLock.cpp。
## 7 參考資料
* [偏向鎖](http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf)
* [java-overview-and-java-se6](http://pdffinder.net/Java-Overview-and-Java-SE-6-What's-New.html)?Synchronization Optimization章節
* Dave Dice?[“Synchronization in Java SE 6”](http://home.comcast.net/~pjbishop/Dave/MustangSync.pdf)
* [Java SE 6 Performance White Paper](http://java.sun.com/performance/reference/whitepapers/6_performance.html#2.1.3)?2.1章節
* [JVM規范(Java SE 7)](http://docs.oracle.com/javase/specs/jvms/se7/html/index.html)
* [Java語言規范(JAVA SE7)](http://docs.oracle.com/javase/specs/jls/se7/html/)
* [周志明的《深入理解Java虛擬機》](http://book.douban.com/subject/6522893/)
* [Java偏向鎖實現原理](http://kenwublog.com/theory-of-java-biased-locking)
## 作者簡介
方騰飛,阿里巴巴資深軟件開發工程師,致力于高性能網絡和并發編程,目前在公司從事詢盤管理和長連接服務器OpenComet的開發工作。 博客地址:[http://ifeve.com](http://ifeve.com/)?微博地址:[http://weibo.com/kirals](http://weibo.com/kirals)
* * *
感謝[張龍](http://www.infoq.com/cn/bycategory.action?authorName=%E5%BC%A0%E9%BE%99)對本文的審校。
給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至[editors@cn.infoq.com](mailto:editors@cn.infoq.com)。也歡迎大家通過新浪微博([@InfoQ](http://www.weibo.com/infoqchina))或者騰訊微博([@InfoQ](http://t.qq.com/infoqchina))關注我們,并與我們的編輯和其他讀者朋友交流。
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)