synchronized想必你們都不陌生,用來解決線程安全問題的利器。同時也是Java高級程序員面試比較常見的面試題。這篇文正會帶你們完全瞭解synchronized的實現。java
想必你們對synchronized都不陌生,主要做用是在多個線程操做共享數據的時候,保證對共享數據訪問的線程安全性。c++
好比在下面這個圖片中,兩個線程對於i這個共享變量同時作i++遞增操做,那麼這個時候對於i這個值來講就存在一個不肯定性,也就是說理論上i的值應該是2,可是也多是1。而致使這個問題的緣由是線程並行執行i++操做並非原子的,存在線程安全問題。因此一般來講解決辦法是經過加鎖來實現線程的串行執行,而synchronized就是java中鎖的實現的關鍵字。程序員
synchronized在併發編程中是一個很是重要的角色,在JDK1.6以前,它是一個重量級鎖的角色,可是在JDK1.6以後對synchronized作了優化,優化之後性能有了較大的提高(這塊會在後面作詳細的分析)。面試
先來看一下synchronized的使用編程
synchronized有三種使用方法,這三種使用方法分別對應三種不一樣的做用域,代碼以下數組
將synchronized修飾在普通同步方法,那麼該鎖的做用域是在當前實例對象範圍內,也就是說對於 SyncDemosd=newSyncDemo();這一個實例對象sd來講,多個線程訪問access方法會有鎖的限制。若是access已經有線程持有了鎖,那這個線程會獨佔鎖,直到鎖釋放完畢以前,其餘線程都會被阻塞安全
public SyncDemo{ Object lock =new Object(); //形式1 public synchronized void access(){ // } //形式2,做用域等同於形式1 public void access1(){ synchronized(lock){ // } } //形式3,做用域等同於前面兩種 public void access2(){ synchronized(this){ // } } }
修飾靜態同步方法或者靜態對象、類,那麼這個鎖的做用範圍是類級別。舉個簡單的例子,微信
SyncDemo sd=SyncDemo(); SyncDemo sd2=new SyncDemo();}
兩個不一樣的實例sd和sd2, 若是sd這個實例訪問access方法而且成功持有了鎖,那麼sd2這個對象若是一樣來訪問access方法,那麼它必需要等待sd這個對象的鎖釋放之後,sd2這個對象的線程才能訪問該方法,這就是類鎖;也就是說類鎖就至關於全局鎖的概念,做用範圍是類級別。多線程
這裏拋一個小問題,你們看看能不能回答,若是不能也不要緊,後面會講解;問題是若是sd先訪問access得到了鎖,sd2對象的線程再訪問access1方法,那麼它會被阻塞嗎?
public SyncDemo{ static Object lock=new Object(); //形式1 public synchronized static void access(){ // } //形式2等同於形式1 public void access1(){ synchronized(lock){ // } } //形式3等同於前面兩種 public void access2(){ synchronzied(SyncDemo.class){ // } } }
public SyncDemo{ Object lock=new Object(); public void access(){ //do something synchronized(lock){ // } } }
經過演示3種不一樣鎖的使用,讓你們對synchronized有了初步的認識。當一個線程視圖訪問帶有synchronized修飾的同步代碼塊或者方法時,必需要先得到鎖。當方法執行完畢退出之後或者出現異常的狀況下會自動釋放鎖。若是你們認真看了上面的三個案例,那麼應該知道鎖的範圍控制是由對象的做用域決定的。對象的做用域越大,那麼鎖的範圍也就越大,所以咱們能夠得出一個初步的猜測,synchronized和對象有很是大的關係。那麼,接下來就去剖析一下鎖的原理
當一個線程嘗試訪問synchronized修飾的代碼塊時,它首先要得到鎖,那麼這個鎖到底存在哪裏呢?
synchronized實現的鎖是存儲在Java對象頭裏,什麼是對象頭呢?在Hotspot虛擬機中,對象在內存中的存儲佈局,能夠分爲三個區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)併發
當咱們在Java代碼中,使用new建立一個對象實例的時候,(hotspot虛擬機)JVM層面實際上會建立一個 instanceOopDesc對象。
Hotspot虛擬機採用OOP-Klass模型來描述Java對象實例,OOP(Ordinary Object Point)指的是普通對象指針,Klass用來描述對象實例的具體類型。Hotspot採用instanceOopDesc和arrayOopDesc來描述對象頭,arrayOopDesc對象用來描述數組類型
instanceOopDesc的定義在Hotspot源碼中的 instanceOop.hpp文件中,另外,arrayOopDesc的定義對應 arrayOop.hpp
class instanceOopDesc : public oopDesc { public: // aligned header size. static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; } // If compressed, the offset of the fields of the instance may not be aligned. static int base_offset_in_bytes() { // offset computation code breaks if UseCompressedClassPointers // only is true return (UseCompressedOops && UseCompressedClassPointers) ? klass_gap_offset_in_bytes() : sizeof(instanceOopDesc); } static bool contains_field_offset(int offset, int nonstatic_field_size) { int base_in_bytes = base_offset_in_bytes(); return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size * heapOopSize); } }; #endif // SHARE_VM_OOPS_INSTANCEOOP_HPP
從instanceOopDesc代碼中能夠看到 instanceOopDesc繼承自oopDesc,oopDesc的定義載Hotspot源碼中的 oop.hpp文件中
class oopDesc { friend class VMStructs; private: volatile markOop _mark; union _metadata { Klass* _klass; narrowKlass _compressed_klass; } _metadata; // Fast access to barrier set. Must be initialized. static BarrierSet* _bs; ... }
在普通實例對象中,oopDesc的定義包含兩個成員,分別是 _mark和 _metadata
_mark表示對象標記、屬於markOop類型,也就是接下來要講解的Mark World,它記錄了對象和鎖有關的信息
_metadata表示類元信息,類元信息存儲的是對象指向它的類元數據(Klass)的首地址,其中Klass表示普通指針、 _compressed_klass表示壓縮類指針
在前面咱們提到過,普通對象的對象頭由兩部分組成,分別是markOop以及類元信息,markOop官方稱爲Mark Word
在Hotspot中,markOop的定義在 markOop.hpp文件中,代碼以下
class markOopDesc: public oopDesc { private: // Conversion uintptr_t value() const { return (uintptr_t) this; } public: // Constants enum { age_bits = 4, //分代年齡 lock_bits = 2, //鎖標識 biased_lock_bits = 1, //是否爲偏向鎖 max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits, hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, //對象的hashcode cms_bits = LP64_ONLY(1) NOT_LP64(0), epoch_bits = 2 //偏向鎖的時間戳 }; ...
Mark word記錄了對象和鎖有關的信息,當某個對象被synchronized關鍵字當成同步鎖時,那麼圍繞這個鎖的一系列操做都和Mark word有關係。Mark Word在32位虛擬機的長度是32bit、在64位虛擬機的長度是64bit。
Mark Word裏面存儲的數據會隨着鎖標誌位的變化而變化,Mark Word可能變化爲存儲如下5中狀況
鎖標誌位的表示意義
到目前爲止,咱們再總結一下前面的內容,synchronized(lock)中的lock能夠用Java中任何一個對象來表示,而鎖標識的存儲實際上就是在lock這個對象中的對象頭內。你們懂了嗎?
其實前面只提到了鎖標誌位的存儲,可是爲何任意一個Java對象都能成爲鎖對象呢?
首先,Java中的每一個對象都派生自Object類,而每一個Java Object在JVM內部都有一個native的C++對象 oop/oopDesc進行對應。
其次,線程在獲取鎖的時候,實際上就是得到一個監視器對象(monitor) ,monitor能夠認爲是一個同步對象,全部的Java對象是天生攜帶monitor.
在hotspot源碼的 markOop.hpp文件中,能夠看到下面這段代碼。
ObjectMonitor* monitor() const { assert(has_monitor(), "check"); // Use xor instead of &~ to provide one extra tag-bit check. return (ObjectMonitor*) (value() ^ monitor_value); }
多個線程訪問同步代碼塊時,至關於去爭搶對象監視器修改對象中的鎖標識,上面的代碼中ObjectMonitor這個對象和線程爭搶鎖的邏輯有密切的關係(後續會詳細分析)
前面提到了鎖的幾個概念,偏向鎖、輕量級鎖、重量級鎖。在JDK1.6以前,synchronized是一個重量級鎖,性能比較差。從JDK1.6開始,爲了減小得到鎖和釋放鎖帶來的性能消耗,synchronized進行了優化,引入了 偏向鎖和 輕量級鎖的概念。因此從JDK1.6開始,鎖一共會有四種狀態,鎖的狀態根據競爭激烈程度從低到高分別是:無鎖狀態->偏向鎖狀態->輕量級鎖狀態->重量級鎖狀態。這幾個狀態會隨着鎖競爭的狀況逐步升級。爲了提升得到鎖和釋放鎖的效率,鎖能夠升級可是不能降級。
下面就詳細講解synchronized的三種鎖的狀態及升級原理
在大多數的狀況下,鎖不只不存在多線程的競爭,並且老是由同一個線程得到。所以爲了讓線程得到鎖的代價更低引入了偏向鎖的概念。偏向鎖的意思是若是一個線程得到了一個偏向鎖,若是在接下來的一段時間中沒有其餘線程來競爭鎖,那麼持有偏向鎖的線程再次進入或者退出同一個同步代碼塊,不須要再次進行搶佔鎖和釋放鎖的操做。偏向鎖能夠經過 -XX:+UseBiasedLocking開啓或者關閉
偏向鎖的獲取過程很是簡單,當一個線程訪問同步塊獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲偏向鎖的線程ID,表示哪一個線程得到了偏向鎖,結合前面分析的Mark Word來分析一下偏向鎖的獲取邏輯
CAS:表示自旋鎖,因爲線程的阻塞和喚醒須要CPU從用戶態轉爲核心態,頻繁的阻塞和喚醒對CPU來講性能開銷很大。同時,不少對象鎖的鎖定狀態指會持續很短的時間,所以引入了自旋鎖,所謂自旋就是一個無心義的死循環,在循環體內不斷的重行競爭鎖。固然,自旋的次數會有限制,超出指定的限制會升級到阻塞鎖。
當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放偏向鎖,撤銷偏向鎖的過程須要等待一個全局安全點(全部工做線程都中止字節碼的執行)。
前面咱們知道,當存在超過一個線程在競爭同一個同步代碼塊時,會發生偏向鎖的撤銷。偏向鎖撤銷之後對象會可能會處於兩種狀態
那麼升級到輕量級鎖之後的加鎖過程和解鎖過程是怎麼樣的呢?
一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於重量級鎖狀態,其餘線程嘗試獲取鎖時,都會被阻塞,也就是 BLOCKED狀態。當持有鎖的線程釋放鎖以後會喚醒這些現場,被喚醒以後的線程會進行新一輪的競爭
![]()
重量級鎖依賴對象內部的monitor鎖來實現,而monitor又依賴操做系統的MutexLock(互斥鎖)
你們若是對MutexLock有興趣,能夠抽時間去了解,假設Mutex變量的值爲1,表示互斥鎖空閒,這個時候某個線程調用lock能夠得到鎖,而Mutex的值爲0表示互斥鎖已經被其餘線程得到,其餘線程調用lock只能掛起等待
爲何重量級鎖的開銷比較大呢?
緣由是當系統檢查到是重量級鎖以後,會把等待想要獲取鎖的線程阻塞,被阻塞的線程不會消耗CPU,可是阻塞或者喚醒一個線程,都須要經過操做系統來實現,也就是至關於從用戶態轉化到內核態,而轉化狀態是須要消耗時間的
到目前爲止,咱們分析了synchronized的使用方法、以及鎖的存儲、對象頭、鎖升級的原理。若是有問題,能夠掃描二維碼留言