在Java中,synchronized
關鍵字是用來控制線程同步的。就是在多線程的環境下,控制synchronized
代碼段不被多個線程同時執行。java
那麼synchronized具體是怎麼作到線程同步的呢?還有鎖升級過程的過程是怎樣的的?咱們來探討一下。linux
咱們先來了看下若是多線程間競爭共享資源,不採起措施會出現什麼狀況:安全
public class TestSync implements Runnable { private int count = 100; public static void main(String[] args) { TestSync ts = new TestSync(); Thread t1 = new Thread(ts, "線程1"); Thread t2 = new Thread(ts, "線程2"); Thread t3 = new Thread(ts, "線程3"); t1.start(); t2.start(); t3.start(); } @Override public void run() { while (true) { if (count > 0) { count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } else { break; } } } }複製代碼
線程2將count減到了97,線程三、線程1在某一刻也作了count--,可是結果卻也是97,說明他們在作count--的時候並不知道有別的線程也操做了count。多線程
這個問題,相信你們都知道加synchronized能夠解決。jvm
對run方法做以下修改:ide
@Overridepublic void run() { while (true) { synchronized (this) { if (count > 0) { count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } else { break; } } } }複製代碼
執行count--有條不紊,不會出現不安全的問題。工具
所以,在代碼層面,加關鍵字synchronized
能解決上述線程安全問題。oop
若是使用IDEA的話,這裏推薦安裝一個jclasslib Bytecode viewer
,這個插件能夠很方便的看程序字節碼執行指令:佈局
咱們來看一下剛纔的程序字節碼指令:測試
實際上synchronized
的實現從字節碼層面來看,就是monitorenter
和monitorexit
指令,這兩個就能夠實現synchronized了。
「monitorenter」:
Java對象天生就是一個Monitor,當monitor被佔用,它就處於鎖定的狀態。
每一個對象都與一個監視器關聯。且只有在有線程持有的狀況下,監視器才被鎖定。
執行monitorenter
的線程嘗試得到monitor的全部權:
「monitorexit」:
一個或多個MonitorExit
指令可與Monitorenter
指令一塊兒使用,它們共同實現同步語句。
儘管能夠將monitorenter
和monitorexit
指令用於提供等效的鎖定語義,但它們並未用於同步方法的實現中。
JVM在完成monitorexit
時的處理方式分爲正常退出和出現異常時退出:
monitorexit
,這個指令是athrow
。monitorexit
。簡單的加鎖解鎖過程
所以,執行同步代碼塊後首先要執行monitorenter
指令,退出的時候monitorexit
指令。
public static void main(String[] args) { Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o) { System.out.println(ClassLayout.parseInstance(o).toPrintable()); } }複製代碼
執行結果:
java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 08 f3 7f 02 (00001000 11110011 01111111 00000010) (41939720) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total複製代碼
沒有加synchronized的時候,對象頭信息的值爲01 00 00 00
,加了鎖以後,對象頭變了08 f3 7f 02
,說明synchronized會修改對象的頭新信息,對象頭在Hotspot裏面叫作markword
。
一個對象的markword
裏面有很是重要的信息,其中最重要的就是鎖synchronized
。(markword裏還有GC的信息,還有hashcode的信息。)
「Hotspot實現的JVM在64位機的markword信息」:
markword信息
在JDK早期的時候,synchronized的底層實現是重量級的,所謂重量級,就是它直接去找操做系統去申請鎖,它的效率是很低的。
JDK後來對synchronized鎖進行了優化,這樣纔有了鎖升級
的概念。
鎖升級
的過程大體是這樣的:
new -> 「偏向鎖」 -> 「輕量級鎖 (自旋鎖)」-> 「重量級鎖」
synchronized優化的過程和markword息息相關。
用markword中最低的三位表明鎖狀態,其中1位是偏向鎖位,最後兩位是普通鎖位。
鎖 = 0 01 無鎖態
❝注意:若是偏向鎖打開,默認是匿名偏向狀態
❞
❝001 + hashcode
❞
00 -> 輕量級鎖
默認狀況,偏向鎖有個時延,默認是4秒
why? 由於JVM虛擬機本身有一些默認啓動的線程,裏面有好多sync代碼,這些sync代碼啓動時就知道確定會有競爭,若是使用偏向鎖,就會形成偏向鎖不斷的進行鎖撤銷和鎖升級的操做,效率較低。
能夠用BiasedLockingStartupDelay參數設置是否啓動偏向鎖(=0,當即啓動偏向鎖):
-XX:BiasedLockingStartupDelay=0複製代碼
鎖升級過程:new Object () - > 101 偏向鎖 ->線程ID爲0 -> Anonymous BiasedLock
打開偏向鎖,new出來的對象,默認就是一個可偏向匿名對象101
上偏向鎖,指的就是,把markword的線程ID改成本身線程ID的過程。
偏向鎖不可重偏向、批量偏向、批量撤銷
撤銷偏向鎖,升級爲輕量級鎖
線程在本身的線程棧生成LockRecord ,用CAS操做將markword設置爲指向本身這個線程的LR的指針,設置成功者獲得鎖
競爭加重:有線程超過10次自旋, (-XX:PreBlockSpin參數可調),或者自旋線程數超過CPU核數的一半, JDK 1.6以後,加入自適應自旋 Adapative Self Spinning ,JVM本身控制。
升級重量級鎖:向操做系統申請資源,linux mutex , CPU從3級-0級系統調用,線程掛起,進入等待隊列,等待操做系統的調度,而後再映射回用戶空間。
總結一下,鎖升級的過程大概是這樣的:
鎖升級過程
自旋是消耗CPU資源的,若是鎖的時間長,或者自旋線程多,CPU會被大量消耗。
重量級鎖有等待隊列,全部拿不到鎖的進入等待隊列,不須要消耗CPU資源
不必定,在明確知道會有多線程競爭的狀況下,偏向鎖確定會涉及鎖撤銷,這時候直接使用自旋鎖。
JVM啓動過程,會有不少線程競爭(明確),因此默認狀況啓動時不打開偏向鎖,過一段兒時間再打開。
在硬件層面,鎖實際上是執行了lock cmpxchg xx
指令。
synchronized在字節碼層面:
若是鎖的是方法,jvm會加一個synchronized修飾符;
若是是同步代碼快,就是用monitorenter和monitorexit指令。
當jvm看到了synchronized修飾符
或者monitorenter和monitorexit
的時候,對應的就是C++調用操做系統提供的同步機制。
CPU級別是使用lock
指令來實現的。
❝好比,咱們要在synchronized某一塊內存上設置一個數i,把i的值從0變成1,這個過程放在CPU執行可能會有好幾條指令或者不能同步(速度太快),因此須要有個lock指令。
cmpxchg前面若是加了一個lock的話,後面的指令執行過程當中對這塊區域進行鎖定,只有這條指令能夠修改,其餘指令是不能操做的。
❞
Java對象頭 「markword」
在Hotspot虛擬機中,對象在內存中的佈局分爲三塊區域:
❝Java對象頭是實現synchronized的鎖對象的基礎,通常而言,synchronized使用的鎖對象是存儲在Java對象頭裏。它是輕量級鎖和偏向鎖的關鍵。
❞
一個同步工具,也能夠描述爲一種同步機制。
爲何每一個對象均可以成爲鎖呢? 由於每一個 Java Object 在 JVM 內部都有一個 native 的 C++ 對象 oop/oopDesc 與之對應,而對應的 oop/oopDesc 都會存在一個markOop 對象頭,而這個對象頭是存儲鎖的位置,裏面還有對象監視器,即ObjectMonitor,因此這也是爲何每一個對象都能成爲鎖的緣由之一。
synchronized的鎖是進行過優化的,引入了偏向鎖、輕量級鎖;鎖的級別從低到高逐步升級, 無鎖->偏向鎖->輕量級鎖->重量級鎖。
當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,只需簡單地測試一下對象頭的markword裏是否存儲着指向當前線程的偏向鎖。
開啓:「-XX:BiasedLockingStartupDelay=0」
自旋等待的時間或者次數是有一個限度的,若是自旋超過了定義的時間仍然沒有獲取到鎖,則該線程應該被掛起。
JDK1.6中-XX:+UseSpinning開啓; -XX:PreBlockSpin=10 爲自旋次數; JDK1.7後,去掉此參數,由jvm控制。
重量級鎖經過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操做系統的Mutex Lock實現,操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。