synchronized是java中加鎖的關鍵字,能夠用來給對象和方法或者代碼塊加鎖,當它鎖定一個方法或者一個代碼塊的時候,同一時刻最多隻有一個線程能夠執行這段代碼。另外一個線程必須等待當前線程執行完這個代碼塊之後才能執行該代碼塊。然而,當一個線程訪問object的一個加鎖代碼塊時,另外一個線程仍能夠訪問該object中的非加鎖代碼塊。java
synchronized關鍵字最主要有如下3種應用方式,下面分別介紹程序員
修飾實例方法:做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖數組
public class AccountingSync implements Runnable{
//共享資源(臨界資源)
static int i=0;
/**
* synchronized 修飾實例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
複製代碼
上述代碼中,咱們開啓兩個線程操做同一個共享資源即變量i,因爲i++;操做並不具有原子性,該操做是先讀取值,而後寫回一個新值,至關於原來的值加上1,分兩步完成,若是第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的域值,那麼第二個線程就會與第一個線程一塊兒看到同一個值,並執行相同值的加1操做,這也就形成了線程安全失敗,所以對於increase方法必須使用synchronized修飾,以便保證線程安全。此時咱們應該注意到synchronized修飾的是實例方法increase,在這樣的狀況下,當前線程的鎖即是實例對象instance,注意Java中的線程同步鎖能夠是任意對象。從代碼執行結果來看確實是正確的,假若咱們沒有使用synchronized關鍵字,其最終輸出結果就極可能小於2000000,這即是synchronized關鍵字的做用。安全
修飾靜態方法:做用於當前類對象(Class對象,每一個類都有一個Class對象),進入同步代碼前要得到當前類對象(Class對象)的鎖bash
public class AccountingSyncClass implements Runnable{
static int i=0;
/**
* 做用於靜態方法,鎖是當前class對象,也就是
* AccountingSyncClass類對應的class對象
*/
public static synchronized void increase(){
i++;
}
/**
* 非靜態,訪問時鎖不同不會發生互斥
*/
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新實例
Thread t1=new Thread(new AccountingSyncClass());
//new新實例
Thread t2=new Thread(new AccountingSyncClass());
//啓動線程
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
複製代碼
因爲synchronized關鍵字修飾的是靜態increase方法,與修飾實例方法不一樣的是,其鎖對象是當前類的class對象。注意代碼中的increase4Obj方法是實例方法,其對象鎖是當前實例對象,若是別的線程調用該方法,將不會產生互斥現象,畢竟鎖對象不一樣,但咱們應該意識到這種狀況下可能會發現線程安全問題(操做了共享靜態變量i)。數據結構
修飾代碼塊:指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。多線程
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
//省略其餘耗時操做....
//使用同步代碼塊對變量i進行同步操做,鎖對象爲instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
複製代碼
從代碼看出,將synchronized做用於一個給定的括號裏的實例對象instance,即當前實例對象就是鎖對象,每次當線程進入synchronized包裹的代碼塊時就會要求當前線程持有instance實例對象鎖,若是當前有其餘線程正持有該對象鎖,那麼新到的線程就必須等待,這樣也就保證了每次只有一個線程執行i++;操做。固然除了instance做爲對象外,咱們還可使用this對象(表明當前實例)或者當前類的class對象做爲鎖,以下代碼:app
//this,當前實例對象鎖
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class對象鎖
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
複製代碼
以上就是java中synchronized關鍵字的用法,很簡單,接下來咱們先介紹一些基礎知識,而後一步一步說明synchronize關鍵字的低層實現原理。ide
HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲三塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
普通對象的對象頭包括兩部分:Mark Word 和 Class Metadata Address (類型指針),若是是數組對象還包括一個額外的Array length數組長度部分。工具
Mark Word:用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等等,佔用內存大小與虛擬機位長一致。
Class Metadata Address:類型指針指向對象的類元數據,虛擬機經過這個指針肯定該對象是哪一個類的實例。
Array length:數組長度
若是對象是數組類型,則虛擬機用3個Word(字寬)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,一字寬等於四字節,即32bit。
長度 | 內容 | 說明 |
---|---|---|
32/64bit | Mark Word | 存儲對象hashCode或鎖信息等運行時數據。 |
32/64bit | Class Metadata Address | 存儲到對象類型數據的指針 |
32/64bit | Array length | 數組的長度(若是當前對象是數組) |
對象須要存儲的運行時數據不少,其實已經超出了3二、64位Bitmap結構所能記錄的限度,可是對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會根據對象的狀態複用本身的存儲空間。例如在32位的HotSpot虛擬機 中對象未被鎖定的狀態下,MarkWord的32個Bits空間中的25Bits用於存儲對象哈希碼(HashCode),4Bits用於存儲對象分代年齡,2Bits用於存儲鎖標誌位,1Bit固定爲0,在其餘狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容以下表所示。
此處可能存在疑問,無鎖狀態時,Mark Word中會存儲hashCode等信息,在有鎖狀態時,位置被鎖指針佔用,那hashCode等信息要存到哪裏?是沒有了嗎?這個問題在後面monitor先關的小節會解答。
什麼是Monitor?咱們能夠把它理解爲一個同步工具,也能夠描述爲一種同步機制,它一般被描述爲一個對象。與一切皆對象同樣,全部的Java對象是天生的Monitor,每個Java對象都有成爲Monitor的潛質,由於在Java的設計中,每個Java對象自打孃胎裏出來就帶了一把看不見的鎖,它叫作內部鎖或者Monitor鎖。
每一個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如monitor能夠與對象一塊兒建立銷燬或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構以下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的)。
ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個數
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
複製代碼
ObjectMonitor中有兩個隊列,_WaitSet和_EntryList,用來保存ObjectWaiter對象列表(每一個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程同時monitor中的計數器count加1,若線程調用wait()方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其餘線程進入獲取monitor(鎖)。以下圖所示
public class SyncCodeBlock {
public int i;
public void syncTask(){
synchronized (this){
i++;
}
}
}
複製代碼
編譯上述代碼並使用javap反編譯後獲得字節碼以下(這裏咱們省略一部分沒有必要的信息):
public class com.fufu.concurrent.SyncCodeBlock {
public int i;
public com.fufu.concurrent.SyncCodeBlock();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void syncTask();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter //注意此處,進入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //注意此處,退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //注意此處,退出同步方法
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
}
複製代碼
從字節碼中可知同步語句塊的實現使用的是monitorenter和monitorexit指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置,當執行monitorenter指令時,當前線程將試圖獲取objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor的進入計數器爲 0,那線程能夠成功取得monitor,並將計數器值設置爲1,取鎖成功。
若是當前線程已經擁有 objectref 的 monitor 的持有權,那它能夠重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。假若其餘線程已經擁有 objectref 的 monitor的全部權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 ,其餘線程將有機會持有 monitor 。
值得注意的是編譯器將會確保不管方法經過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而不管這個方法是正常結束仍是異常結束。爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然能夠正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理全部的異常,它的目的就是用來執行 monitorexit 指令。從字節碼中也能夠看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。
public class SyncMethod {
public int i;
public synchronized void syncTask(){
i++;
}
}
複製代碼
方法級的同步是隱式,即無需經過字節碼指令來控制的,它實如今方法調用和返回操做之中。JVM能夠從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當方法調用時,調用指令將會 檢查方法的 ACC_SYNCHRONIZED訪問標誌是否被設置,若是設置了,執行線程將先持有monitor(虛擬機規範中用的是管程一詞),而後再執行方法,最後再方法完成(不管是正常完成仍是非正常完成)時釋放monitor。在方法執行期間,執行線程持有了monitor,其餘任何線程都沒法再得到同一個monitor。若是一個同步方法執行期間拋出了異常,而且在方法內部沒法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法以外時自動釋放。下面咱們看看字節碼層面如何實現:
//省略不必的字節碼
//==================syncTask方法======================
public synchronized void syncTask();
descriptor: ()V
//方法標識ACC_PUBLIC表明public修飾,ACC_SYNCHRONIZED指明該方法爲同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 12: 0
line 13: 10
複製代碼
從字節碼中能夠看出,synchronized修飾的方法並無monitorenter指令和monitorexit指令,取得代之的確實是ACC_SYNCHRONIZED標識,該標識指明瞭該方法是一個同步方法,JVM經過該ACC_SYNCHRONIZED訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。這即是synchronized鎖在同步代碼塊和同步方法上實現的基本原理。
上一節看出,Synchronized的實現依賴於與某個對象向關聯的monitor(監視器)實現,而monitor是基於底層操做系統的Mutex Lock實現的,而基於Mutex Lock實現的同步必須經歷從用戶態到核心態的轉換,這個開銷特別大,成本很是高。因此頻繁的經過Synchronized實現同步會嚴重影響到程序效率,而這種依賴於Mutex Lock實現的鎖機制也被稱爲「重量級鎖」,爲了減小重量級鎖帶來的性能開銷,JDK對Synchronized進行了種種優化。
Java SE1.6爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,因此在Java SE1.6裏鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。
在看下面內容以前,若是不熟悉CAS是什麼的話,強烈建議看一下這篇關於CAS機制的博客,java的鎖優化基本上就是基於CAS,對於理解下面內容有很大幫助。《深刻淺出CAS》
Hotspot的做者通過以往的研究發現大多數狀況下鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低而引入了偏向鎖。
獲取鎖:釋放鎖: 偏向鎖的釋放採用了一種只有競爭纔會釋放鎖的機制,線程是不會主動去釋放偏向鎖,須要等待其餘線程來競爭。偏向鎖的撤銷須要等待全局安全點(這個時間點是上沒有正在執行的代碼)。其步驟以下:
此時,解答一下前面的小節中提出的問題,在有鎖狀態時,位置被鎖指針佔用,那hashCode等信息要存到哪裏?是沒有了嗎?通過在網上苦苦搜尋,終於找到了大神關於次問題的恢復,下面先看偏向鎖的狀況,偏向鎖時,mark word中記錄了線程id,沒有足夠的額外空間存儲hashcode,因此,答案是:
請必定要注意,這裏討論的hash code都只針對identity hash code。用戶自定義的hashCode()方法所返回的值跟這裏討論的不是一回事。Identity hash code是未被覆寫的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。
由於mark word裏沒地方同時放bias信息和identity hash code。 HotSpot VM是假定「實際上只有不多對象會計算identity hash code」來作優化的;換句話說若是實際上有不少對象都計算了identity hash code的話,HotSpot VM會被迫使用比較不優化的模式。
做者:RednaxelaFX 連接:www.zhihu.com/question/52… 來源:知乎
引入輕量級鎖的主要目的是在多沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。 當關閉偏向鎖功能或者多個線程競爭偏向鎖致使偏向鎖升級爲輕量級鎖,則會嘗試獲取輕量級鎖,其步驟以下:
獲取鎖:
釋放鎖: 輕量級鎖的釋放也是經過CAS操做來進行的,主要步驟以下:
輕量級鎖狀態時,位置被鎖指針佔用,那hashCode等信息要存到哪裏?這裏的問題就比較簡單了,由於有拷貝的mark word,因此Displaced Mark Word中存在所須要的信息。
重量級鎖經過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操做系統的Mutex Lock實現,操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。
輕量級鎖失敗後,虛擬機爲了不線程真實地在操做系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。這是基於在大多數狀況下,線程持有鎖的時間都不會太長,若是直接掛起操做系統層面的線程可能會得不償失,畢竟操做系統實現線程之間的切換時須要從用戶態轉換到核心態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,所以自旋鎖會假設在不久未來,當前的線程能夠得到鎖,所以虛擬機會讓當前想要獲取鎖的線程作幾個空循環(這也是稱爲自旋的緣由),通常不會過久,多是50個循環或100循環,在通過若干次循環後,若是獲得鎖,就順利進入臨界區。若是還不能得到鎖,那就會將線程在操做系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是能夠提高效率的。最後沒辦法也就只能升級爲重量級鎖了。自旋是把雙刃劍,若是旋的時間過長會影響總體性能,時間太短又達不到延遲阻塞的目的。顯然,自旋的週期選擇顯得很是重要,但這與操做系統、硬件體系、系統的負載等諸多場景相關,很難選擇,若是選擇不當,不但性能得不到提升,可能還會降低。
JDK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。所謂自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。它怎麼作呢?線程若是自旋成功了,那麼下次自旋的次數會更加多,由於虛擬機認爲既然上次成功了,那麼這次自旋也頗有可能會再次成功,那麼它就會容許自旋等待持續的次數更多。反之,若是對於某個鎖,不多有自旋可以成功的,那麼在之後要或者這個鎖的時候自旋的次數會減小甚至省略掉自旋過程,以避免浪費處理器資源。
有了自適應自旋鎖,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的情況預測會愈來愈準確,虛擬機會變得愈來愈聰明。
爲了保證數據的完整性,咱們在進行操做時須要對這部分操做進行同步控制,可是在有些狀況下,JVM檢測到不可能存在共享數據競爭,這是JVM會對這些同步鎖進行鎖消除。鎖消除的依據是逃逸分析的數據支持。 若是不存在競爭,爲何還須要加鎖呢?因此鎖消除能夠節省毫無心義的請求鎖的時間。變量是否逃逸,對於虛擬機來講須要使用數據流分析來肯定,可是對於咱們程序員來講這還不清楚麼?咱們會在明明知道不存在數據競爭的代碼塊前加上同步嗎?可是有時候程序並非咱們所想的那樣?咱們雖然沒有顯示使用鎖,可是咱們在使用一些JDK的內置API時,如StringBuffer、Vector、HashTable等,這個時候會存在隱形的加鎖操做。好比StringBuffer的append()方法,Vector的add()方法。
在前面偏向鎖和輕量級鎖的小節中已經大概瞭解的鎖的膨脹流程:
偏向鎖->輕量級鎖->重量級鎖
偏向所鎖,輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。
一個對象剛開始實例化的時候,沒有任何線程來訪問它的時候。它是可偏向的,意味着,它如今認爲只可能有一個線程來訪問它,因此當第一個線程來訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。
偏向第一個線程,這個線程在修改對象頭成爲偏向鎖的時候使用CAS操做,並將對象頭中的ThreadID改爲本身的ID,以後再次訪問這個對象時,只須要對比ID,不須要再使用CAS在進行操做。
一旦有第二個線程訪問這個對象,由於偏向鎖不會主動釋放,因此第二個線程能夠看到對象是偏向狀態,這時代表在這個對象上已經存在競爭了,檢查原來持有該對象鎖的線程是否依然存活,若是掛了,則能夠將對象變爲無鎖狀態,而後從新偏向新的線程,若是原來的線程依然存活,則立刻執行那個線程的操做棧,檢查該對象的使用狀況,若是仍然須要持有偏向鎖,則偏向鎖升級爲輕量級鎖,(偏向鎖就是這個時候升級爲輕量級鎖的)。若是不存在使用了,則能夠將對象回覆成無鎖狀態,而後從新偏向。
輕量級鎖認爲競爭存在,可是競爭的程度很輕,通常兩個線程對於同一個鎖的操做都會錯開,或者說稍微等待一下(自旋),另外一個線程就會釋放鎖。 可是當自旋超過必定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹爲重量級鎖,重量級鎖使除了擁有鎖的線程之外的線程都阻塞,防止CPU空轉。
下面這張圖,很好的說明了鎖的膨脹流程。