互斥同步html
互斥同步(Mutual Exclusion & Synchronization)是常見的一種併發正確性保證手段。同步是指子啊多個線程併發訪問共享數據時,保證共享數據在同一時刻只能被一個(或者是一些,使用信號量的時候)線程使用。而互斥是實現同步的一種手段,臨界區(Critial Section)、互斥量(Mutex)和信號量(Semaphore)都是主要的互斥實現方式。所以,在這四個字裏面,互斥是因,同步是果;互斥是方法,同步是目的。java
synchronized的實現程序員
在Java中,你們都知道,synchronized關鍵字是最基本的互斥同步手段。看一段簡單的代碼:併發
public static void main(String[] args) { synchronized (TestMain.class) { } }
這段代碼被編譯以後是這樣的:app
1 public static void main(java.lang.String[]); 2 flags: ACC_PUBLIC, ACC_STATIC 3 Code: 4 stack=2, locals=1, args_size=1 5 0: ldc #1 // class com/xrq/test53/TestMain 6 2: dup 7 3: monitorenter 8 4: monitorexit 9 5: return 10 LineNumberTable: 11 line 7: 0 12 line 11: 5 13 LocalVariableTable: 14 Start Length Slot Name Signature 15 0 6 0 args [Ljava/lang/String;
關鍵就在第7行和第8行,在源代碼被編譯以後,Java虛擬機會利用monitorenter和monitorexit條字節碼指令來處理synchronized這個關鍵字。性能
根據虛擬機規範的要求,在執行monitorenter指令時,首先要嘗試獲取對象的鎖,若是這個對象沒有被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1,相應地,在執行monitorexit指令時會將鎖計數器減1,當計數器爲0時,鎖就會被釋放。若是獲取對象鎖失敗,那當前線程就要阻塞等待,直到對象鎖被另一個線程釋放爲止。spa
關於monitorenter和monitorexit,有兩點是要特別注意的:操作系統
一、synchronized同步塊對同一條線程來講是可重入的,不會出現把本身鎖死的問題線程
二、同步塊在已進入的線程執行完以前,會阻塞後面其它線程的進入3d
由於Java的線程是映射到操做系統的原生線程之上的,若是要阻塞或者喚醒一個線程,都須要操做系統來幫忙完成,這就須要從用戶態轉換到核心態中,所以狀態轉換須要耗費不少的處理器時間,對於代碼簡單的同步塊,狀態轉換消耗的時間有可能比用戶代碼執行的時間還長,因此synchronized是Java語言中一個重量級(Heavyweight)鎖,有經驗的程序員都會在確實必要的狀況下才使用這種操做。
順便看一下HotSpot虛擬機對象頭Mark Word:
存 儲 內 存 | 標 識 位 | 狀 態 |
對象哈希嗎、對象分代年齡 | 01 | 未鎖定 |
指向鎖記錄的指針 | 00 | 輕量級鎖定 |
指向重量級鎖的指針 | 10 | 膨脹(重量級鎖定) |
空,不須要記錄信息 | 11 | GC標記 |
偏向線程ID、偏向時間戳、對象分代年齡 | 01 | 可偏向 |
看到有一個重量級鎖定,指的就是重量級鎖。
volatile的實現
對於volatile關鍵字,一個被volatile關鍵字修飾的變量,在生成彙編語言以後,大體會多出這麼一條指令:
0x01a3de24:lock addl $0x0,(%esp) ;...f0830424 00
這個操做至關因而一個內存屏障,只有一個CPU訪問內存時,並不須要內存屏障;但若是有兩個或者更多CPU訪問同一塊內存時,且其中一個在觀測另一個,就須要內存屏障來保證一致性了。這句指令中的"addl $0x0,(%esp)"(把esp寄存器的值加0)顯然是一個空操做(採用這個空操做而不是空指令nop是由於IA32手冊規定lock前綴不容許配合nop指令使用),關鍵在於lock前綴,查詢IA32手冊,它的做用是使得本CPU的Cache寫入了內存,該寫入動做也會引發別的CPU或者別的內核無效化其Cache,這種操做至關於對Cache中的變量作了一次"store和write"操做,因此經過這樣一個空操做,可以讓前面volatile變量的修改對其餘CPU當即可見。
自旋鎖與自適應自旋
互斥同步,對性能影響最大的是阻塞的實現,掛起線程和恢復線程的操做都須要轉入內核狀態完成,這些操做給系統的併發性能帶來了很大的壓力。同時,虛擬機開發團隊也注意到不少應用上,共享數據的鎖定狀態只會持續很短的一段時間,爲了這段時間去掛起和恢復線程並不值得。若是物理機上有一個以上的處理器,能讓兩個或兩個以上的線程同時並行執行,咱們就可讓後面請求鎖的那個線程"稍等一下",但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。爲了讓線程等待,咱們只須要讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。
在JDK1.4.2就已經引入了自旋鎖,只不過默認是關閉的。自旋不能代替阻塞,且先不說處理器數量的要求,自旋等待自己雖然避免了線程切換的開銷,可是它是要佔據處理器時間的,所以若是鎖被佔用的時間很短,自旋等待的效果就很是好;反之,若是鎖被佔用的時間很長,那麼自旋的線程只會白白消耗處理器資源,而不會作任何有用的工做,反而會帶來性能上的浪費。所以自選等待必須有必定的限度,若是自旋超過了限定的次數仍然沒有成功得到鎖,就應當使用傳統的方式去掛起線程了,自旋次數的默認值是10。
在JDK1.6以後引入了自適應的自旋鎖。自適應意味着自旋的時間再也不固定了,而是由前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態來決定。若是在同一個鎖對象上,自旋等待剛剛得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間,好比100個循環。另外若是對於某一個鎖,自旋不多成功得到過,那麼在之後要得到這個鎖時將可能忽略掉自旋過程,以免浪費處理器資源。有了自適應自旋,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的情況預測就會愈來愈準確。
鎖消除
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要斷定依據來源於逃逸分析的支持,若是判斷在一段代碼中,堆上全部數據都不會逃逸出去從而被其餘線程訪問到,那就能夠把它們當作棧上數據對待,認爲它們是線程私有的,同步加鎖天然無需進行。
鎖粗化
原則上,咱們在編寫代碼的時候,老是推薦將同步塊的做用範圍限制得儘可能小----只在共享數據的實際做用域中才進行同步,這樣是爲了使得須要同步的操做數儘量變小,若是存在鎖競爭,那等待鎖的線程也能儘快拿到鎖。
大部分狀況下,上面的原則都是正確的,可是若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做是出如今循環體中的,那即便沒有線程競爭,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。
若是這麼說不夠直觀,那麼想一想某段代碼反覆使用StringBuffer的append方法拼接字符串的例子吧。