Java鎖細節整理

前言

鎖是在開發的過程沒法避免的問題。也是面試常問的問題。 本章比較詳細的解決了java中的鎖,記住是鎖。html

一. JDK8存在的鎖

  1. synchronized
  2. StampedLock
  3. ReentrantLock
  4. ReentrantReadWriteLock

PS: 下面內容測試的結果不是十分正確。第一,測試的jdk是1.6,而不是1.8.測試的沒有關閉UseBiasedLocking(偏向鎖)java

二,鎖的特性

  1. 鎖升級(十二分)

多種強度的鎖方式。優先使用性能消耗低的方式,噹噹前方式不可用或者不使用,改變成消耗比當前大的鎖方式。 是一種十分靈活,智能的性能優化方案,對非特定場景十分適合。不須要開發者關注鎖優化,只須要關注業務程序員

  1. 重入(十二分)

同一個鎖, 重入是應付鎖複雜使用狀況下的一把利器,是鎖性能重點。a與b方式使用同一個鎖。a調用b,須要兩個鎖指令,而重入的解決方案是a執行鎖指令,當a調用b方法的時候,不使用鎖指令。那麼鎖的開銷減小一半。重入得越多鎖開銷減小越多。 有測試哪裏,須要你們本身測試。 下面的連接中性能測試其實已經給了答案,不知道哪位大神,能夠把答案告訴打雜的老胡面試

  1. 讀寫(六分)

分讀寫鎖,讀鎖與讀鎖之間能夠共享。在讀多寫少的場景,提升鎖的性能 下面博客有對讀寫的性能測試:http://www.inter12.org/archives/292spring

  1. 公平

爲了性能,不按上鎖的循序得到鎖,即不公平鎖。按照上鎖的循序得到鎖,即公平鎖。 公平不是爲了優先級 下面博客有對公平的性能測試:https://yq.aliyun.com/articles/48612sql

  1. 自動釋放(十二分)

不用手動調用unLock系列釋放鎖的方法。解決在複雜的開發體系(業務複雜,開發人員能力良莠不齊,細節無視與混淆,測試困難)中,鎖操做問題。 異常釋放 誰來釋放數據庫

  1. 鎖等待(六分)

當其餘線程得到鎖以後,等待固定時間以後,尚未得到鎖就不在爭奪鎖。json

  1. 線程中斷(一分)

三,開發難度

特性支持
synchronized StampedLock ReentrantLock ReentrantReadWriteLock
鎖升級 支持 支持 不支持 不支持
重入 支持 不支持 可支持 可支持
讀寫 不支持 支持 不支持 支持
公平 不支持 不支持 支持 支持
自動釋放 支持 不支持 不支持 不支持
鎖等待超時 不支持 支持 支持 支持
線程中斷 不支持 支持 支持 支持
實際使用 簡單首選 不建議使用 比較簡單 當心使用
開發人員使用分類
synchronized StampedLock ReentrantLock ReentrantReadWriteLock 自研
初級開發人員 使用 不使用 不使用 不使用 不使用
中級開發人員 使用 不使用 不支持 不使用 不使用
高級開發人員 使用 不使用 不支持 不使用 不使用
研發人員 使用 不使用 使用 使用 使用
資深研發人員 使用 使用 使用 使用 使用
鎖應用環境
synchronized StampedLock ReentrantLock ReentrantReadWriteLock 自研
業務系 使用 不使用 不使用 不使用 不使用
功能系 使用 不使用 使用 不使用 不使用
軟件系 使用 不使用 使用 使用 使用

業務模塊基本不須要關注鎖,須要鎖的地方都,高級如下程序員都使用的數據庫鎖,或者其餘庫鎖。打雜的目前在業務系沒有用過java鎖, 功能模塊的開發與維護基本是高級或者研發人員,用到鎖的地方很少,絕對大部分是使用的synchronized與ReentrantLock,在spring的代碼裏面只見到許多synchronized,ReentrantLock還沒見過 軟件繫好比netty,dubbo,大量使用ReentrantLock,一些獨立的服務好比rocketmq,核心業務重寫了ReentrantLock,還有設計本身的加鎖機制性能優化

讀寫類型
synchronized StampedLock ReentrantLock ReentrantReadWriteLock 自研
讀多 不使用 不使用 不使用 使用 不使用
各半 使用 不使用 不使用 使用 不使用
寫多 使用 不使用 使用 不使用 不使用
全寫 使用 不使用 使用 不使用 不使用

全寫操做目前發現最多的就是計數器,計數器建議使用jdk8的LongAdders(計數器),性能超級好。注意任何計數器沒法保證絕對的精確性 ** ReentrantLock與ReentrantReadWriteLock的寫性能同樣**服務器

總結

若是對特性重要進行排序的話我認爲是 非對性能極限要求的

重入>鎖升級>自動釋放>鎖等待超時>公平>讀寫>線程中斷

  1. 在繁多,複雜的方法,代碼,邏輯之間相互調用。誰也不知道,哪一個方法,哪段代碼使用了鎖,一不當心死鎖。因此重入是最重要的一點。 除非資深研發人員不然其餘人員不該該使用StampedLock

  2. 鎖升級能夠作基本性能方面優化,就交給鎖了,可讓鎖性能在個個場景均可以保持較好的狀態,從而減小鎖開發與維護的工做量

  3. 自動釋放對初級,中級或者高級開發來講,是一個避免出現鎖問題的利器,保障開發簡單,順利。不用擔憂哪裏忘記釋放鎖,從而形成鎖問題

  4. 鎖等待超時是防止無限鎖等待而形成線程資源無限佔用與線程池無線程可用的狀況,從而讓應用沒法提供服務。是高可用服務保障的利器

  5. 複雜的環境下,不知道哪一個方法,哪一個代碼使用了讀鎖仍是寫鎖。太多未知與細節,十分頭疼,須要大量的時間與精力處理讀寫關係,得不償失。

作了這麼多年開發與研發。感受性能較好的狀況下,不出問題與開發維護方便應該放在對性能高度最求的前面。尤爲是線上問題,應該避免出現。
從上面的對比分析,synchronized的得分與評價是最高的,ReentrantLock其次, 不建議使用ReentrantReadWriteLock,禁止使用StampedLock。

四,synchronized詳析

  1. synchronized 是java關鍵字,jvm內部實現。因此jvm能夠對synchronized進行優化
  2. 每一個jdk版本synchronized性能不同,版本越高的性能越好。jdk1.6與jdk1.7之間的性能差距十分大。
  3. synchronized操做簡單,jvm自動優化性能

synchronized詳析鎖的方式

public class SynchronizedLockMode {
	private static int increment = 0;
	
	private Object object = new Object( );
	
	public synchronized void lockMethod(){
		print("lockMethod");
	}
	
	public synchronized static void  lockStaticMethod(){
		print("lockStaticMethod");
	}
	
	public void lockBlock(){
		synchronized( object ){
			print("lockBlock");
		}
	}
}

運行代碼

@Test
	public void synchronizedLockModeTest(){
		SynchronizedLockMode   slm = new SynchronizedLockMode();
		Thread thread1 = new Thread( new Runnable( ) {
			public void run ( ) {
				slm.lockMethod( );
			}
		} );
		Thread thread2 = new Thread( new Runnable( ) {
			public void run ( ) {	
				SynchronizedLockMode.lockStaticMethod( );
			}
		} );
		Thread thread3 = new Thread( new Runnable( ) {
			public void run ( ) {	
				slm.lockBlock( );
			}
		} );
		thread1.start( );
		thread2.start( );
		thread3.start( );
		try {
			Thread.sleep( 1000 );
		} catch ( InterruptedException e ) {
			// TODO 自動生成的 catch 塊
			e.printStackTrace();
		}
}

運行結果

lockMethod:  0  for num0
lockBlock:  1  for num0
lockStaticMethod:  0  for num0
lockBlock:  3  for num1
lockMethod:  2  for num1
lockBlock:  5  for num2
lockStaticMethod:  4  for num1
lockBlock:  7  for num3
lockMethod:  6  for num2
lockBlock:  9  for num4
lockStaticMethod:  8  for num2
lockBlock:  11  for num5
lockMethod:  10  for num3
lockBlock:  13  for num6
lockStaticMethod:  12  for num3
lockBlock:  15  for num7
lockBlock:  17  for num8
lockMethod:  14  for num4
lockBlock:  18  for num9
lockBlock:  20  for num10
lockBlock:  21  for num11
lockStaticMethod:  16  for num4
lockBlock:  22  for num12
lockMethod:  19  for num5
lockBlock:  24  for num13
lockStaticMethod:  23  for num5
lockBlock:  26  for num14
lockMethod:  25  for num6
lockBlock:  28  for num15
lockStaticMethod:  27  for num6
lockBlock:  30  for num16
lockMethod:  29  for num7
lockBlock:  32  for num17
lockStaticMethod:  31  for num7
lockBlock:  34  for num18
lockMethod:  33  for num8
lockBlock:  36  for num19
lockStaticMethod:  35  for num8
lockStaticMethod:  38  for num9
lockStaticMethod:  39  for num10
lockStaticMethod:  40  for num11
lockStaticMethod:  41  for num12
lockStaticMethod:  42  for num13
lockMethod:  37  for num9
lockStaticMethod:  43  for num14
lockMethod:  44  for num10
lockStaticMethod:  45  for num15
lockMethod:  46  for num11
lockStaticMethod:  47  for num16
lockMethod:  48  for num12
lockStaticMethod:  49  for num17
lockMethod:  50  for num13
lockStaticMethod:  51  for num18
lockMethod:  52  for num14
lockStaticMethod:  53  for num19
lockMethod:  54  for num15
lockMethod:  55  for num16
lockMethod:  56  for num17
lockMethod:  57  for num18
lockMethod:  58  for num19

從上面的執行能夠是否發現一個問題,答應是亂序的,自增數據是亂序的。不少人認爲:絕對是java設計的失誤....... //???使用一個圖片

來邏輯推理下:

  1. java與jvm絕對沒有錯
  2. synchronized是上鎖,這點絕對沒有問題
  3. 那synchronized鎖了什麼了?

這是咱們討論的論題,也是一個不當心容易犯錯的問題。

演示代碼,有四個方法。

public synchronized void lockThisObject(){
		sleep("synchronized method");
	}
	
	public void VerificationLockMethodIsWhatObject(){
		synchronized( this ){
			sleep("synchronized block lock this" , false);
		}
	}
	
	
	public synchronized static void  lockClassObject(){
		sleep("synchronized method static ");
	}
	
	public void VerificationLockStaticMethodIsWhatObject(){
		synchronized( SynchronizedLockMode.class ){
			sleep("synchronized block lock SynchronizedLockMode.class" , false);
		}
	}
	
	private static void sleep(String lock , boolean boo){
		if(boo){
			sleep( lock );
		}else{
			System.out.println( lock + "  execute" ) ;
		}
	}
	
	private static void sleep(String lock){
		sleep0( lock );
	}
	
	private static void sleep0(String lock){
		try {
			System.out.println( lock + "  start sleep" ) ;
			Thread.sleep( 10000 );
			System.out.println( lock + "  end sleep" ) ;
		} catch ( InterruptedException e ) {
			// TODO 自動生成的 catch 塊
			e.printStackTrace();
		}
}

代碼解讀   有四個方法分別是靜態方法,非靜態方法,兩個方法裏面有synchronized block。   四個方法分別組合,測試方法的互斥行。輸出內容是按照調用方法的循序執行的,synchronized block方法的輸出結果在synchronized 方法以後,那麼表示兩個方法是互斥的   組合: 1. 鎖靜態方法 塊鎖鎖住this 2. 鎖靜態方法 塊鎖鎖住Class 3. 鎖非靜態方法 塊鎖鎖住this 4. 鎖非靜態方法 塊鎖鎖住Class

test代碼與結果 第一個組合

SynchronizedLockMode   slm = new SynchronizedLockMode();
Thread thread1 = new Thread( new Runnable( ) {
			public void run ( ) {
					SynchronizedLockMode.lockClassObject( );
			}
} );
Thread thread2 = new Thread( new Runnable( ) {
			public void run ( ) {
					slm.verificationLockMethodIsWhatObject( );
			}
			} );
System.out.println( "第一個組合 鎖靜態方法 塊鎖鎖住this" ) ;
thread1.start( );
sleep();//暫停1秒,是爲了線程執行不亂
thread2.start( );
sleep(10000);// 暫停10秒,是由於鎖方法會展廳5秒,同時爲輸出內容不會與上面組合亂
System.out.println( "第一個組合 執行結束" ) ;

第一個組合 鎖靜態方法 塊鎖鎖住this
synchronized method static   start sleep
synchronized block lock this  execute
synchronized method static   end sleep
第一個組合 執行結束

第二個組合

thread1 = new Thread( new Runnable( ) {
		public void run ( ) {
		SynchronizedLockMode.lockClassObject( );
		}
} );
thread2 = new Thread( new Runnable( ) {
		public void run ( ) {
		slm.verificationLockStaticMethodIsWhatObject( );
		}
} );
System.out.println( "第二個組合 鎖靜態方法 塊鎖鎖住Class" ) ;
thread1.start( );
sleep();
thread2.start( );
sleep(10000);
System.out.println( "第二個組合 執行結束" ) ;

第二個組合 鎖靜態方法 塊鎖鎖住Class
synchronized method static   start sleep
synchronized method static   end sleep
synchronized block lock SynchronizedLockMode.class  execute
第二個組合 執行結束

第三個組合

thread1 = new Thread( new Runnable( ) {
			public void run ( ) {
			slm.lockThisObject( );
			}
} );
thread2 = new Thread( new Runnable( ) {
		public void run ( ) {
		slm.verificationLockMethodIsWhatObject( );
		}
} );
System.out.println( "第三個組合 鎖非靜態方法 塊鎖鎖住this" ) ;
thread1.start( );
sleep();
thread2.start( );
sleep(10000);
System.out.println( "第三個組合 執行結束" ) ;

第三個組合 鎖非靜態方法 塊鎖鎖住this
synchronized method  start sleep
synchronized method  end sleep
synchronized block lock this  execute
第三個組合 執行結束

第四個組合

thread1 = new Thread( new Runnable( ) {
			public void run ( ) {
				slm.lockThisObject( );
			}
 } );
thread2 = new Thread( new Runnable( ) {
			public void run ( ) {
				slm.verificationLockStaticMethodIsWhatObject( );
			}
 } );
 System.out.println( "第四個組合 非鎖靜態方法 塊鎖鎖住Class" ) ;
 thread1.start( );
 sleep();
 thread2.start( );
 sleep(10000);
 System.out.println( "第四個組合 執行結束" ) ;

第四個組合 非鎖靜態方法 塊鎖鎖住Class
synchronized method  start sleep
synchronized block lock SynchronizedLockMode.class  execute
synchronized method  end sleep
第四個組合 執行結束

經過結果分析會發現第二,三組合的輸出結果是有序的。麻煩在看看二,三組合調用的方法 第二個組合是: 鎖靜態方法 塊鎖鎖住Class 第三個組合是: 鎖非靜態方法 塊鎖鎖住this

結論:

  synchronized關鍵字標記在靜態方法上是鎖當前的class對象

public synchronized static void XXXXX(){
	// 鎖的對象是 當前的 class
}

  synchronized關鍵字標記在非靜態方法上是鎖當前的實例對象(this)

public synchronized  void XXXXX(){
	// 鎖的對象是 當前的 this
}

總結

jdk版本越高synchronized的性能越好

這是阿里大神fashjson與driud的做者溫 對synchronized簡單總結 1

背景

   某日,對某個優化過的系統進行,上線前的性能壓測,壓測結果大大的出乎人意料,優化以後比優化以前的TPS只多200+。在16cpu的服務器不該該出現這樣的狀況。

問題排查

  是否是接口中數據庫操做的問題,MySQL通用日誌裏記錄的sql基本一致,慢日誌裏面沒有記錄接口操做的sql。是否是測試人員的測試數據十分重複,更新操做形成鎖超時,準備排除鎖超時狀況,測試人員與業務開發人員反饋,查詢接口也同樣,數據狀態良好   是否是代碼問題,分析最後的此時結果,發現全部壓測接口都這樣。包括簡單條主鍵查詢的SQL   Why? 奇蹟了,數據庫與應用一切正常啊。被逼無賴,在每一個核心地方輸出調用時間,也沒問題。發現全部的接口的使用了RateLimiter的acquire方法,深刻一看,有synchronized。每次接口的調用都會進入下面的代碼,而每次都會有鎖爭奪。

2

解決方案

高併發下synchronize形成jvm性能消耗詳析

jvm對synchronized代碼塊的優化

google guava 的RateLimiter 限流的核心計算代碼使用的synchronized,google大神都證實了synchronized的優秀

公平鎖與不公平鎖

  1. 在通常競爭狀況下,二者的性能能夠理解爲相等
  2. 在極高競爭下,不公平鎖的性能是多是公平鎖的十幾倍

ReentrantReadWriteLock 死鎖現象

背景
某個深夜,老胡在看ReentrantReadWriteLock源碼,想用ReentrantReadWriteLock代替ReentrantLock提升性能,反覆的看調用流程與實現細節(看了兩個多小時),腦海慢慢呈現整個調用流程與實現細節的流程與邏輯圖,發現不對勁啊,可能一不當心出現死鎖
public void reentrantReadWriteLock() throws InterruptedException{
		ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
		ReadLock  readLock  = rrwl.readLock( );
		WriteLock wrtieLock = rrwl.writeLock( );
		
		readLock.lock( );
		readLock.lock( );
		readLock.unlock( );
		readLock.unlock( );
		
		wrtieLock.lock( );
		wrtieLock.lock( );
		wrtieLock.unlock( );
		wrtieLock.unlock( );
		
		wrtieLock.lock( );
		readLock.lock( );
		wrtieLock.unlock( );
		readLock.unlock( );
		
		readLock.lock( );
		//wrtieLock.lock( );
		wrtieLock.lockInterruptibly( );
		readLock.unlock( );
		wrtieLock.unlock( );
	}
通過測試以後,老胡出了一點冷汗,這個死鎖隱藏得太深了。還好是老胡慢慢,慢,看出來了,以老胡的編碼方式,還真得出現這樣的死鎖

ReentrantLock 與 ReentrantReadWriteLock 在高併發下的不公平鎖 出現餓死現象

在發現死鎖現象同一個深夜,老胡在仔細反覆的看公佈與不公平,讀寫鎖的細節。反覆的看調用流程與實現細節,一邊準備與周公喝茶了,一個低頭砸到桌子上,腦海裏整個調用流程與實現細節的流程與邏輯圖砸出一個閃光,找到一個問題
	在高併發下,不少線程爭奪一個鎖的時候,在隊列的裏面的鎖可能能難爭奪到鎖,爭奪不到,會餓死啊。
鎖方式 ReentrantLock ReentrantReadWriteLock
lock 飢餓 飢餓
lockInterruptibly 飢餓 飢餓
tryLock 不飢餓 不飢餓
tryLock(超時) 超時餓醒 超時餓醒

在高併發狀況下,使用tryLock(超時)杜絕 飢餓。沒得到鎖,能夠直接異常與返回異常結果

鎖使用總結

  1. 能不使用鎖,絕對不要使用...........
  2. 注意上面說的細節,好比synchronized鎖的對象等
  3. 優先使用synchronized關鍵字,能不用使用synchronized塊就不使用
  4. 高併發的狀況下使用synchronized,麻煩關閉偏向鎖-XX:-UseBiasedLocking
  5. 減小鎖粒度 2
  6. 優先使用重入鎖,禁止非重入鎖StampedLock的使用
  7. 必定要分析場景,在選擇對應的鎖,若是不分析只能使用synchronized
  8. tryLock(超時) 是處理死鎖與飢餓的神器。
  9. 一個class或者一個實例裏面只容許一個鎖。兩個鎖容易出現死鎖。這個鎖必須能重入
private Object object = new Object();

public void a(){
	synchronized(object){
	
	}
}
public void b(){
	synchronized(object){
	
	}
}
// c 方法與a,b方法的鎖不是一個,在這個類裏面有兩個鎖分別是 object與this,
public void c(){
	synchronized(this){
	
	}
}
// 同時存在 a b鎖,很容易不當心死鎖。
ReentrantLock a= new ReentrantLock();
ReentrantLock b= new ReentrantLock();

至此 2018年01月25日00.26. 歷經一個多月,才寫完。也真的佩服文字表達能力與技術描述能力,1個多月啊。

歡迎大神蝌蚪,轉載。大神蝌蚪的網址是www.kedou.me

相關文章
相關標籤/搜索