【強烈推薦!非廣告!】阿里雲雙11褥羊毛活動: https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.hf47liqn 差很少一折,不過僅限阿里雲新人購買,不是新人的朋友本身找方法買哦!
Github 地址:https://github.com/Snailclimb/JavaGuide/edit/master/Java相關/synchronized.mdjava
下面我已一個常見的面試題爲例講解一下 synchronized 關鍵字的具體使用。git
面試中面試官常常會說:「單例模式瞭解嗎?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實現單利模式的原理唄!」github
雙重校驗鎖實現對象單例(線程安全)面試
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判斷對象是否已經實例過,沒有實例化過才進入加鎖代碼 if (uniqueInstance == null) { //類對象加鎖 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
另外,須要注意 uniqueInstance 採用 volatile 關鍵字修飾也是頗有必要。安全
uniqueInstance 採用 volatile 關鍵字修飾也是頗有必要的, uniqueInstance = new Singleton(); 這段代碼實際上是分爲三步執行:多線程
可是因爲 JVM 具備指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出先問題,可是在多線程環境下會致使一個線程得到尚未初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 後發現 uniqueInstance 不爲空,所以返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。ide
使用 volatile 能夠禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。性能
synchronized 關鍵字底層原理屬於 JVM 層面。優化
① synchronized 同步語句塊的狀況ui
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("synchronized 代碼塊"); } } }
經過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關字節碼信息:首先切換到類的對應目錄執行 javac SynchronizedDemo.java
命令生成編譯後的 .class 文件,而後執行javap -c -s -v -l SynchronizedDemo.class
。
從上面咱們能夠看出:
synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。 當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在於每一個Java對象的對象頭中,synchronized 鎖即是經過這種方式獲取鎖的,也是爲何Java中任意對象能夠做爲鎖的緣由) 的持有權.當計數器爲0則能夠成功獲取,獲取後將鎖計數器設爲1也就是加1。相應的在執行 monitorexit 指令後,將鎖計數器設爲0,代表鎖被釋放。若是獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另一個線程釋放爲止。
② synchronized 修飾方法的的狀況
public class SynchronizedDemo2 { public synchronized void method() { System.out.println("synchronized 方法"); } }
synchronized 修飾的方法並無 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 經過該 ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。
在 Java 早期版本中,synchronized 屬於重量級鎖,效率低下,由於監視器鎖(monitor)是依賴於底層的操做系統的 Mutex Lock 來實現的,Java 的線程是映射到操做系統的原生線程之上的。若是要掛起或者喚醒一個線程,都須要操做系統幫忙完成,而操做系統實現線程之間的切換時須要從用戶態轉換到內核態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,這也是爲何早期的 synchronized 效率低的緣由。慶幸的是在 Java 6 以後 Java 官方對從 JVM 層面對synchronized 較大優化,因此如今的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減小鎖操做的開銷。
JDK1.6 對鎖的實現引入了大量的優化,如偏向鎖、輕量級鎖、自旋鎖、適應性自旋鎖、鎖消除、鎖粗化等技術來減小鎖操做的開銷。
鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級。注意鎖能夠升級不可降級,這種策略是爲了提升得到鎖和釋放鎖的效率。
①偏向鎖
引入偏向鎖的目的和引入輕量級鎖的目的很像,他們都是爲了沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。可是不一樣是:輕量級鎖在無競爭的狀況下使用 CAS 操做去代替使用互斥量。而偏向鎖在無競爭的狀況下會把整個同步都消除掉。
偏向鎖的「偏」就是偏愛的偏,它的意思是會偏向於第一個得到它的線程,若是在接下來的執行中,該鎖沒有被其餘線程獲取,那麼持有偏向鎖的線程就不須要進行同步!關於偏向鎖的原理能夠查看《深刻理解Java虛擬機:JVM高級特性與最佳實踐》第二版的13章第三節鎖優化。
可是對於鎖競爭比較激烈的場合,偏向鎖就失效了,由於這樣場合極有可能每次申請鎖的線程都是不相同的,所以這種場合下不該該使用偏向鎖,不然會得不償失,須要注意的是,偏向鎖失敗後,並不會當即膨脹爲重量級鎖,而是先升級爲輕量級鎖。
② 輕量級鎖
假若偏向鎖失敗,虛擬機並不會當即升級爲重量級鎖,它還會嘗試使用一種稱爲輕量級鎖的優化手段(1.6以後加入的)。輕量級鎖不是爲了代替重量級鎖,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗,由於使用輕量級鎖時,不須要申請互斥量。另外,輕量級鎖的加鎖和解鎖都用到了CAS操做。 關於輕量級鎖的加鎖和解鎖的原理能夠查看《深刻理解Java虛擬機:JVM高級特性與最佳實踐》第二版的13章第三節鎖優化。
輕量級鎖可以提高程序同步性能的依據是「對於絕大部分鎖,在整個同步週期內都是不存在競爭的」,這是一個經驗數據。若是沒有競爭,輕量級鎖使用 CAS 操做避免了使用互斥操做的開銷。但若是存在鎖競爭,除了互斥量開銷外,還會額外發生CAS操做,所以在有鎖競爭的狀況下,輕量級鎖比傳統的重量級鎖更慢!若是鎖競爭激烈,那麼輕量級將很快膨脹爲重量級鎖!
③ 自旋鎖和自適應自旋
輕量級鎖失敗後,虛擬機爲了不線程真實地在操做系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。
互斥同步對性能最大的影響就是阻塞的實現,由於掛起線程/恢復線程的操做都須要轉入內核態中完成(用戶態轉換到內核態會耗費時間)。
通常線程持有鎖的時間都不是太長,因此僅僅爲了這一點時間去掛起線程/恢復線程是得不償失的。 因此,虛擬機的開發團隊就這樣去考慮:「咱們能不能讓後面來的請求獲取鎖的線程等待一會而不被掛起呢?看看持有鎖的線程是否很快就會釋放鎖」。爲了讓一個線程等待,咱們只須要讓線程執行一個忙循環(自旋),這項技術就叫作自旋。
百度百科對自旋鎖的解釋:
何謂自旋鎖?它是爲實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較相似,它們都是爲了解決對某項資源的互斥使用。不管是互斥鎖,仍是自旋鎖,在任什麼時候刻,最多隻能有一個保持者,也就說,在任什麼時候刻最多隻能有一個執行單元得到鎖。可是二者在調度機制上略有不一樣。對於互斥鎖,若是資源已經被佔用,資源申請者只能進入睡眠狀態。可是自旋鎖不會引發調用者睡眠,若是自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是所以而得名。
自旋鎖在 JDK1.6 以前其實就已經引入了,不過是默認關閉的,須要經過--XX:+UseSpinning
參數來開啓。JDK1.6及1.6以後,就改成默認開啓的了。須要注意的是:自旋等待不能徹底替代阻塞,由於它仍是要佔用處理器時間。若是鎖被佔用的時間短,那麼效果固然就很好了!反之,相反!自旋等待的時間必需要有限度。若是自旋超過了限定次數任然沒有得到鎖,就應該掛起線程。自旋次數的默認值是10次,用戶能夠修改--XX:PreBlockSpin
來更改。
另外,在 JDK1.6 中引入了自適應的自旋鎖。自適應的自旋鎖帶來的改進就是:自旋的時間不在固定了,而是和前一次同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,虛擬機變得愈來愈「聰明」了。
④ 鎖消除
鎖消除理解起來很簡單,它指的就是虛擬機即便編譯器在運行時,若是檢測到那些共享數據不可能存在競爭,那麼就執行鎖消除。鎖消除能夠節省毫無心義的請求鎖的時間。
⑤ 鎖粗化
原則上,咱們再編寫代碼的時候,老是推薦將同步快的做用範圍限制得儘可能小——只在共享數據的實際做用域才進行同步,這樣是爲了使得須要同步的操做數量儘量變小,若是存在鎖競爭,那等待線程也能儘快拿到鎖。
大部分狀況下,上面的原則都是沒有問題的,可是若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,那麼會帶來不少沒必要要的性能消耗。
① 二者都是可重入鎖
二者都是可重入鎖。「可重入鎖」概念是:本身能夠再次獲取本身的內部鎖。好比一個線程得到了某個對象的鎖,此時這個對象鎖尚未釋放,當其再次想要獲取這個對象的鎖的時候仍是能夠獲取的,若是不可鎖重入的話,就會形成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增1,因此要等到鎖的計數器降低爲0時才能釋放鎖。
② synchronized 依賴於 JVM 而 ReenTrantLock 依賴於 API
synchronized 是依賴於 JVM 實現的,前面咱們也講到了 虛擬機團隊在 JDK1.6 爲 synchronized 關鍵字進行了不少優化,可是這些優化都是在虛擬機層面實現的,並無直接暴露給咱們。ReenTrantLock 是 JDK 層面實現的(也就是 API 層面,須要 lock() 和 unlock 方法配合 try/finally 語句塊來完成),因此咱們能夠經過查看它的源代碼,來看它是如何實現的。
③ ReenTrantLock 比 synchronized 增長了一些高級功能
相比synchronized,ReenTrantLock增長了一些高級功能。主要來講主要有三點:①等待可中斷;②可實現公平鎖;③可實現選擇性通知(鎖能夠綁定多個條件)
ReentrantLock(boolean fair)
構造方法來制定是不是公平的。若是你想使用上述功能,那麼選擇ReenTrantLock是一個不錯的選擇。
④ 性能已不是選擇標準
在JDK1.6以前,synchronized 的性能是比 ReenTrantLock 差不少。具體表示爲:synchronized 關鍵字吞吐量歲線程數的增長,降低得很是嚴重。而ReenTrantLock 基本保持一個比較穩定的水平。我以爲這也側面反映了, synchronized 關鍵字還有很是大的優化餘地。後續的技術發展也證實了這一點,咱們上面也講了在 JDK1.6 以後 JVM 團隊對 synchronized 關鍵字作了不少優化。JDK1.6 以後,synchronized 和 ReenTrantLock 的性能基本是持平了。因此網上那些說由於性能才選擇 ReenTrantLock 的文章都是錯的!JDK1.6以後,性能已經不是選擇synchronized和ReenTrantLock的影響因素了!並且虛擬機在將來的性能改進中會更偏向於原生的synchronized,因此仍是提倡在synchronized能知足你的需求的狀況下,優先考慮使用synchronized關鍵字來進行同步!優化後的synchronized和ReenTrantLock同樣,在不少地方都是用到了CAS操做。