在多線程併發編程中synchronized一直是元老級角色,不少人都會稱呼它爲重量級鎖。可是,隨着Java SE 1.6對synchronized進行了各類優化以後,有些狀況下它就並不那麼重了。java
synchronized能夠修飾普通方法,靜態方法和代碼塊。當synchronized修飾一個方法或者一個代碼塊的時候,它可以保證在同一時刻最多隻有一個線程執行該段代碼。數據庫
對於普通同步方法,鎖是當前實例對象(不一樣實例對象之間的鎖互不影響)。編程
對於靜態同步方法,鎖是當前類的Class對象。數組
對於同步方法塊,鎖是Synchonized括號裏配置的對象。安全
當一個線程試圖訪問同步代碼塊時,它首先必須獲得鎖,退出或拋出異常時必須釋放鎖。多線程
synchronized最經常使用的使用場景就是多線程併發編程時線程的同步。這邊仍是舉一個最經常使用的列子:多線程狀況下銀行帳戶存錢和取錢的列子。併發
public class SynchronizedDemo { public static void main(String[] args) { BankAccount myAccount = new BankAccount("accountOfMG",10000.00); for(int i=0;i<100;i++){ new Thread(new Runnable() { @Override public void run() { try { int var = new Random().nextInt(100); Thread.sleep(var); } catch (InterruptedException e) { e.printStackTrace(); } double deposit = myAccount.deposit(1000.00); System.out.println(Thread.currentThread().getName()+" balance:"+deposit); } }).start(); } for(int i=0;i<100;i++){ new Thread(new Runnable() { @Override public void run() { try { int var = new Random().nextInt(100); Thread.sleep(var); } catch (InterruptedException e) { e.printStackTrace(); } double deposit = myAccount.withdraw(1000.00); System.out.println(Thread.currentThread().getName()+" balance:"+deposit); } }).start(); } } private static class BankAccount{ String accountName; double balance; public BankAccount(String accountName,double balance){ this.accountName = accountName; this.balance = balance; } public double deposit(double amount){ balance = balance + amount; return balance; } public double withdraw(double amount){ balance = balance - amount; return balance; } } }
上面的列子中,首先初始化了一個銀行帳戶,帳戶的餘額是10000.00,而後開始了200個線程,其中100個每次向帳戶中存1000.00,另外100個每次從帳戶中取1000.00。若是正常執行的話,帳戶中應該仍是10000.00。可是咱們執行屢次這段代碼,會發現執行結果基本上都不是10000.00,並且每次結果 都是不同的。框架
出現上面這種結果的緣由就是:在多線程狀況下,銀行帳戶accountOfMG
是一個共享變量,對共享變量進行修改若是不作線程同步的話是會存在線程安全問題的。好比說如今有兩個線程同時要對帳戶accountOfMG
存款1000,一個線程先拿到帳戶的當前餘額,而且將餘額加上1000。可是還沒將餘額的值刷新回帳戶,另外一個線程也來作相同的操做。此時帳戶餘額仍是沒加1000以前的值,因此當兩個線程執行完畢以後,帳戶加的總金額仍是隻有1000。dom
synchronized就是Java提供的一種線程同步機制。使用synchronized咱們能夠很是方便地解決上面的銀行帳戶多線程存錢取錢問題,只須要使用synchronized修飾存錢和取錢方法便可:ide
private static class BankAccount{ String accountName; double balance; public BankAccount(String accountName,double balance){ this.accountName = accountName; this.balance = balance; } //這邊給出一個編程建議:當咱們對共享變量進行同步時,同步代碼塊最好在共享變量中加 public synchronized double deposit(double amount){ balance = balance + amount; return balance; } public synchronized double withdraw(double amount){ balance = balance - amount; return balance; } }
上面提到,當線程進入synchronized方法或者代碼塊時須要先獲取鎖,退出時須要釋放鎖。那麼這個鎖信息到底存在哪裏呢?
Java對象保存在內存中時,由如下三部分組成:
而對象頭又由下面幾部分組成:
1. Mark Word
Mark Word記錄了對象和鎖有關的信息,當這個對象被synchronized關鍵字當成同步鎖時,圍繞這個鎖的一系列操做都和Mark Word有關。Mark Word在32位JVM中的長度是32bit,在64位JVM中長度是64bit。
Mark Word在不一樣的鎖狀態下存儲的內容不一樣,在32位JVM中是這麼存的:
其中無鎖和偏向鎖的鎖標誌位都是01,只是在前面的1bit區分了這是無鎖狀態仍是偏向鎖狀態。Epoch是指偏向鎖的時間戳。
JDK1.6之後的版本在處理同步鎖時存在鎖升級的概念,JVM對於同步鎖的處理是從偏向鎖開始的,隨着競爭愈來愈激烈,處理方式從偏向鎖升級到輕量級鎖,最終升級到重量級鎖。
JVM通常是這樣使用鎖和Mark Word的:
step1:當沒有被當成鎖時,這就是一個普通的對象,Mark Word記錄對象的HashCode,鎖標誌位是01,是否偏向鎖那一位是0。
step2:當對象被當作同步鎖並有一個線程A搶到了鎖時,鎖標誌位仍是01,可是否偏向鎖那一位改爲1,前23bit記錄搶到鎖的線程id,表示進入偏向鎖狀態。
step3:當線程A再次試圖來得到鎖時,JVM發現同步鎖對象的標誌位是01,是否偏向鎖是1,也就是偏向狀態,Mark Word中記錄的線程id就是線程A本身的id,表示線程A已經得到了這個偏向鎖,能夠執行同步鎖的代碼。
step4:當線程B試圖得到這個鎖時,JVM發現同步鎖處於偏向狀態,可是Mark Word中的線程id記錄的不是B,那麼線程B會先用CAS操做試圖得到鎖,這裏的得到鎖操做是有可能成功的,由於線程A通常不會自動釋放偏向鎖。若是搶鎖成功,就把Mark Word裏的線程id改成線程B的id,表明線程B得到了這個偏向鎖,能夠執行同步鎖代碼。若是搶鎖失敗,則繼續執行步驟5。
step5:偏向鎖狀態搶鎖失敗,表明當前鎖有必定的競爭,偏向鎖將升級爲輕量級鎖。JVM會在當前線程的線程棧中開闢一塊單獨的空間,裏面保存指向對象鎖Mark Word的指針,同時在對象鎖Mark Word中保存指向這片空間的指針。上述兩個保存操做都是CAS操做,若是保存成功,表明線程搶到了同步鎖,就把Mark Word中的鎖標誌位改爲00,能夠執行同步鎖代碼。若是保存失敗,表示搶鎖失敗,競爭太激烈,繼續執行步驟6。
step6:輕量級鎖搶鎖失敗,JVM會使用自旋鎖,自旋鎖不是一個鎖狀態,只是表明不斷的重試,嘗試搶鎖。從JDK1.7開始,自旋鎖默認啓用,自旋次數由JVM決定。若是搶鎖成功則執行同步鎖代碼,若是失敗則繼續執行步驟7。
step7:自旋鎖重試以後若是搶鎖依然失敗,同步鎖會升級至重量級鎖,鎖標誌位改成10。在這個狀態下,未搶到鎖的線程都會被阻塞。
2. 指向類的指針
該指針在32位JVM中的長度是32bit,在64位JVM中長度是64bit。Java對象的類數據保存在方法區。
3. 數組長度
只有數組對象保存了這部分數據。該數據在32位和64位JVM中長度都是32bit。
Java 6中爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」的概念。在Java 6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。
在聊偏向鎖、輕量級鎖和重量級鎖以前咱們先來聊下鎖的宏觀分類。鎖從宏觀上來分類,能夠分爲悲觀鎖與樂觀鎖。注意,這裏說的的鎖能夠是數據庫中的鎖,也能夠是Java等開發語言中的鎖技術。悲觀鎖和樂觀鎖其實只是一類概念(對某類具體鎖的總稱),不是某種語言或是某個技術獨有的鎖技術。
樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採起在寫時先讀出當前版本號,而後加鎖操做(比較跟上一次的版本號,若是同樣則更新),若是失敗則要重複讀-比較-寫的操做。java中的樂觀鎖基本都是經過CAS操做實現的,CAS是一種更新的原子操做,比較當前值跟傳入值是否同樣,同樣則更新,不然失敗。數據庫中的共享鎖也是一種樂觀鎖。
悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,因此每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中典型的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如ReentrantLock。數據庫中的排他鎖也是一種悲觀鎖。
Java 6以前的synchronized會致使爭用不到鎖的線程進入阻塞狀態,線程在阻塞狀態和runnbale狀態之間切換是很耗費系統資源的,因此說它是java語言中一個重量級的同步操縱,被稱爲重量級鎖。爲了緩解上述性能問題,Java 6開始,引入了輕量鎖與偏向鎖,默認啓用了自旋,他們都屬於樂觀鎖。
偏向鎖更準確的說是鎖的一種狀態。在這種鎖狀態下,系統中只有一個線程來爭奪這個鎖。線程只要簡單地經過Mark Word中存放的線程ID和本身的ID是否一致就能拿到鎖。下面簡單介紹下偏向鎖獲取和升級的過程。
仍是就着這張圖講吧,會清楚點。
當系統中尚未訪問過synchronized代碼時,此時鎖的狀態確定是「無鎖狀態」,也就是說「是不是偏向鎖」的值是0,「鎖標誌位」的值是01。此時有一個線程1來訪問同步代碼,發現鎖對象的狀態是"無鎖狀態",那麼操做起來很是簡單了,只須要將「是否偏向鎖」標誌位改爲1,再將線程1的線程ID寫入Mark Word便可。
若是後續系統中一直只有線程1來拿鎖,那麼只要簡單的判斷下線程1的ID和Mark Word中的線程ID,線程1就能很是輕鬆地拿到鎖。可是現實每每不是那麼簡單的,如今假設線程2也要來競爭同步鎖,咱們看下狀況是怎麼樣的。
在JDK6中,偏向鎖是默認啓用的。它提升了單線程訪問同步資源的性能。但試想一下,若是你的同步資源或代碼一直都是多線程訪問的,那麼消除偏向鎖這一步驟對你來講就是多餘的。事實上,消除偏向鎖的開銷仍是蠻大的。
因此在你很是熟悉本身的代碼前提下,大可禁用偏向鎖:
-XX:-UseBiasedLocking=false
"輕量級鎖"鎖也是一種鎖的狀態,這種鎖狀態的特色是:當一個線程來競爭鎖失敗時,不會當即進入阻塞狀態,而是會進行一段時間的鎖自旋操做,若是自旋操做拿鎖成功就執行同步代碼,若是通過一段時間的自旋操做仍是沒拿到鎖,線程就進入阻塞狀態。
1. 輕量級鎖加鎖流程
線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。而後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖,若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
2. 輕量級鎖解鎖流程
輕量級解鎖時,會使用原子的CAS操做將Displaced Mark Word替換回到對象頭,若是成功,則表示沒有競爭發生。若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。
由於自旋會消耗CPU,爲了不無用的自旋(好比得到鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其餘線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖以後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。
自旋鎖原理很是簡單,若是持有鎖的線程能在很短期內釋放鎖資源,那麼那些等待競爭鎖的線程就不須要作內核態和用戶態之間的切換進入阻塞掛起狀態,它們只須要等一等(自旋),等持有鎖的線程釋放鎖後便可當即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
可是線程自旋是須要消耗CPU的,說白了就是讓CPU在作無用功,線程不能一直佔用CPU自旋作無用功,因此須要設定一個自旋等待的最大時間。若是持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會致使其它爭用鎖的線程在最大等待時間內仍是獲取不到鎖,這時爭用線程會中止自旋進入阻塞狀態。
自旋鎖儘量的減小線程的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間很是短的代碼塊來講性能能大幅度的提高,由於自旋的消耗會小於線程阻塞掛起操做的消耗!可是若是鎖的競爭激烈,或者持有鎖的線程須要長時間佔用鎖執行同步塊,這時候就不適合使用自旋鎖了,由於自旋鎖在獲取鎖前一直都是佔用cpu作無用功,線程自旋的消耗大於線程阻塞掛起操做的消耗,其它須要cup的線程又不能獲取到cpu,形成cpu的浪費。
JDK7以後,鎖的自旋特性都是由JVM自身控制的,不須要咱們手動配置。