瞭解Java中的鎖,看這一篇就夠了!

1 Lock接口

鎖是用來控制多個線程訪問同一個共享資源的方式,通常來講,一個鎖能防止多個線程同時訪問共享資源,在Lock接口出來以前,Java是經過synchronized關鍵字來實現鎖的功能,而Java1.5以後,併發包新增了Lock接口(以及相關實現類)用來實現鎖的功能,它提供了與synchronized關鍵字相似的同步功能,只是在使用方式上有所不一樣,須要顯式的獲取鎖和釋放鎖。雖然缺乏了隱式的便捷性,但卻擁有了鎖獲取和釋放的可操做性,可中斷的獲取因此及超時獲取鎖的的同步特性java

1.1 Lock接口提供的synchronized不具有的特性:

特性 描述
嘗試非阻塞式獲取鎖 當前線程嘗試獲取鎖,若是這一刻沒有被其餘線程獲取到,則成功獲取並持有鎖
能被中斷的獲取鎖 與synchronized關鍵字不一樣,獲取到鎖的線程可以響應中斷,當獲取到鎖的線程被中斷時,中斷異常會被拋出,同時鎖會被釋放
超時獲取鎖 在指定的截止時間以前獲取到鎖,入夥截止時間到了仍舊沒法獲取鎖,則返回

1.2 Lock接口 API

方法名稱 描述
void lock() 獲取鎖,調用該方法當前線程將會獲取鎖,當鎖獲取到時,從該方法返回
void lockInterruptibly() throws InterruptedException() 可中斷的獲取鎖,和lock()方法的不一樣之處在於該方法可響應中斷,即在鎖的獲取中和中斷當前線程
boolean tryLock() 嘗試非阻塞的獲取鎖,調用該方法馬上返回,若是可以獲取則返回true,不然返回false
boolean tryLock(long time,TimeUnit unit) throws InterruptedException() 超時的獲取鎖,當前線程在如下三種狀況會返回:1.當前線程在超時時間內獲取到鎖 2. 當前線程在超時時間內被中斷 3. 超時時間結束,返回false
void unlock() 釋放鎖
Condition newCondition() 獲取等待通知組件,該組件和當前的鎖綁定,當前線程只有得到了鎖,才能調用該組件的wait()方法,而調用後,當前線程釋放鎖

1.3 AbstractQueueSynchronized(隊列同步器)

如下簡稱AQS AQS是用來構建鎖和其餘同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,經過內置的FIFO隊列來完成資源獲取線程排隊工做redis

1.3.1 AQS API

AQS給予模板方法設計模式設計的,也就是說,使用者須要繼承AQS並重寫指定的方法進行實現算法

AQS提供以下三個方法來訪問和修改同步狀態:數據庫

  • getState():獲取當前線程的同步狀態。
  • getState(int newState):設置當前線程同步狀態。
  • compareAndSetState(int expect,int update):使用CAS設置當前狀態,該方法可以保證狀態設置的原子性

1.3.2 AQS可重寫的方法

方法名稱 描述
protected boolean tryAcquire(int arg) 獨佔式的獲取同步狀態,實現該方法須要查詢當前狀態並判斷同步狀態是否符合預期,而後再進行CAS設置同步狀態
protected boolean tryRelease(int arg) 獨佔式釋放同步狀態,等待獲取同步狀態的線程將有機會獲取同步狀態
protected int tryAcquireShared(int arg) 共享式獲取同步狀態,返回大於等於0的值,表示獲取成功,反之獲取失敗
protected boolean tryReleaseShared(int arg) 共享式釋放同步狀態
protected boolean isHeldExclusively() 當前同步器是否在獨佔模式下被線程佔用,通常該方法表示是否被當前線程所獨佔

1.3.3 AQS提供的模板方法

方法名稱 描述
void acquire(int arg) 獨佔式獲取同步狀態,若是當前線程獲取同步狀態成功,則該方法返回,不然,將進入同步隊列等待,該方法將會調用重寫的tryAcquire(int arg)方法
void acquireInterruptibly(int arg) 與acquire(int arg)相同,可是該方法響應中斷,當前線程未獲取到同步狀態而進入同步隊列中,若是當前線程被中斷,則該方法拋出異常並返回
boolean tryAcquireNanos(int arg,long nanos) 在acquireInterruptibly(int arg)基礎上增長了超時限制,若是當前線程在超時時間內沒有獲取到同步狀態,那麼將會返回false,若是獲取到了則返回true
void acquireShared(int arg) 共享式的獲取同步狀態,若是當前線程未獲取到同步狀態,將會進入同步隊列中進行等待,與獨佔式的區別主要在於同一時刻能夠有多個線程獲取到同步狀態
void acquireSharedInterruptibly(int arg) 與acquireInterruptibly(int arg)相同,該方法可響應中斷
boolean tryAcquireSharedNanos(int arg,long nanos) 在acquireSharedInterruptibly(int arg)基礎上增長了超時限制
boolean release(int arg) 獨佔式的釋放同步狀態,該方法會在釋放同步狀態以後,將同步隊列中的第一個節點包含的線程喚醒
boolean releaseShared(int arg) 共享式的釋放同步狀態
Collection getQueueThreads() 獲取等待在同步隊列上的線程集合

1.3.4 獨佔鎖和共享鎖的區別

  • 獨佔鎖,顧名思義,就是在同一時刻只能有一個線程獲取到鎖,而其餘的線程只能在同步隊列中等待,只有獲取鎖的線程釋放了鎖,後繼線程才能獲取到鎖
  • 共享鎖就是在同一時刻能夠有多個線程獲取鎖
  • 獨享鎖與共享鎖也是經過AQS來實現的,經過實現不同的方法,來實現獨享或者共享

2 常見的鎖

2.1 重入鎖

重入鎖,也叫作遞歸鎖,指的是同一線程外層函數得到鎖以後,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。編程

在JAVA環境下ReentrantLock和sypnchronized都是可重入鎖設計模式

public class Test implements Runnable {
	public synchronized void get() {
		System.out.println("name:" + Thread.currentThread().getName() + " get();");
		set();
	}
	public synchronized void set() {
		System.out.println("name:" + Thread.currentThread().getName() + " set();");
	}
	@Override
	public void run() {
		get();
	}
	public static void main(String[] args) {
		Test ss = new Test();
		new Thread(ss).start();
		new Thread(ss).start();
		new Thread(ss).start();
		new Thread(ss).start();
	}
}
複製代碼
public class Test02 extends Thread {
	ReentrantLock lock = new ReentrantLock();
	public void get() {
		lock.lock();
		System.out.println(Thread.currentThread().getId());
		set();
		lock.unlock();
	}
	public void set() {
		lock.lock();
		System.out.println(Thread.currentThread().getId());
		lock.unlock();
	}
	@Override
	public void run() {
		get();
	}
	public static void main(String[] args) {
		Test ss = new Test();
		new Thread(ss).start();
		new Thread(ss).start();
		new Thread(ss).start();
	}
}
複製代碼

2.2 讀寫鎖

相比Java中的鎖(Locks in Java)裏Lock實現,讀寫鎖更復雜一些。假設你的程序中涉及到對一些共享資源的讀和寫操做,且寫操做沒有讀操做那麼頻繁。在沒有寫操做的時候,兩個線程同時讀一個資源沒有任何問題,因此應該容許多個線程能在同時讀取共享資源。可是若是有一個線程想去寫這些共享資源,就不該該再有其它線程對該資源進行讀或寫(譯者注:也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存)。 這就須要一個讀/寫鎖來解決這個問題。Java5在java.util.concurrent包中已經包含了讀寫鎖。儘管如此,咱們仍是應該瞭解其實現背後的原理。緩存

public class Cache {
	static Map<String, Object> map = new HashMap<String, Object>();
	static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	static Lock r = rwl.readLock();
	static Lock w = rwl.writeLock();
	// 獲取一個key對應的value
	public static final Object get(String key) {
		r.lock();
		try {
			System.out.println("正在作讀的操做,key:" + key + " 開始");
			Thread.sleep(100);
			Object object = map.get(key);
			System.out.println("正在作讀的操做,key:" + key + " 結束");
			System.out.println();
			return object;
		} catch (InterruptedException e) {
		} finally {
			r.unlock();
		}
		return key;
	}
	// 設置key對應的value,並返回舊有的value
	public static final Object put(String key, Object value) {
		w.lock();
		try {
			System.out.println("正在作寫的操做,key:" + key + ",value:" + value + "開始.");
			Thread.sleep(100);
			Object object = map.put(key, value);
			System.out.println("正在作寫的操做,key:" + key + ",value:" + value + "結束.");
			System.out.println();
			return object;
		} catch (InterruptedException e) {
		} finally {
			w.unlock();
		}
		return value;
	}
	// 清空全部的內容
	public static final void clear() {
		w.lock();
		try {
			map.clear();
		} finally {
			w.unlock();
		}
	}
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					Cache.put(i + "", i + "");
				}
			}
		}).start();
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					Cache.get(i + "");
				}
			}
		}).start();
	}
}
複製代碼

2.3 樂觀鎖

老是認爲不會產生併發問題,每次去取數據的時候總認爲不會有其餘線程對數據進行修改,所以不會上鎖,可是在更新時會判斷其餘線程在這以前有沒有對數據進行修改,通常會使用版本號機制或CAS操做實現,本質沒有鎖,效率比較高,無阻塞,無等待,重試安全

實現方式服務器

  • version方式:通常是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加1。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,不然重試更新操做,直到更新成功。 核心SQL語句
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
複製代碼
  • CAS操做方式:即compare and swap 或者 compare and set,涉及到三個操做數,數據所在的內存值,預期值,新值。當須要更新時,判斷當前內存值與以前取到的值是否相等,若相等,則用新值更新,若失敗則重試,通常狀況下是一個自旋操做,即不斷的重試。

2.4 悲觀鎖

老是假設最壞的狀況,每次取數據時都認爲其餘線程會修改,因此都會加鎖(讀鎖、寫鎖、行鎖等),當其餘線程想要訪問數據時,都須要阻塞掛起。能夠依靠數據庫實現,如行鎖、讀鎖和寫鎖等,都是在操做以前加鎖,在Java中,synchronized的思想也是悲觀鎖。屬於重量級鎖,會阻塞,會等待併發

2.5 synchronized

  • 優勢
  1. 具備可重入性,保證原子性和可見性
  • 缺點
  1. 鎖的本質是重量級鎖,開銷大,不能禁止重排序,產生阻塞,效率低下

2.6 分佈式鎖

若是想在不一樣的jvm中保證數據同步,使用分佈式鎖技術。有數據庫實現、緩存redis實現、Zookeeper分佈式鎖

2.7 自旋鎖和互斥鎖的區別

  • 自旋鎖(Spin lock) 自旋鎖與互斥鎖有點相似,只是自旋鎖不會引發調用者睡眠,若是自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是 否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是所以而得名。其做用是爲了解決某項資源的互斥使用。由於自旋鎖不會引發調用者睡眠,因此自旋鎖的效率遠 高於互斥鎖。雖然它的效率比互斥鎖高,可是它也有些不足之處:
  1. 自旋鎖一直佔用CPU,他在未得到鎖的狀況下,一直運行--自旋,因此佔用着CPU,若是不能在很短的時 間內得到鎖,這無疑會使CPU效率下降。
  2. 在用自旋鎖時有可能形成死鎖,當遞歸調用時有可能形成死鎖,調用有些其餘函數也可能形成死鎖,如 copy_to_user()、copy_from_user()、kmalloc()等。

所以咱們要慎重使用自旋鎖,自旋鎖只有在內核可搶佔式或SMP的狀況下才真正須要,在單CPU且不可搶佔式的內核下,自旋鎖的操做爲空操做。自旋鎖適用於鎖使用者保持鎖時間比較短的狀況下。

  • 兩種鎖的加鎖原理
  1. 互斥鎖:線程會從sleep(加鎖)——>running(解鎖),過程當中有上下文的切換,cpu的搶佔,信號的發送等開銷。
  2. 自旋鎖:線程一直是running(加鎖——>解鎖),死循環檢測鎖的標誌位,機制不復雜。 互斥鎖屬於sleep-waiting類型的鎖。例如在一個雙核的機器上有兩個線程(線程A和線程B),它們分別運行在Core0和 Core1上。假設線程A想要經過pthread_mutex_lock操做去獲得一個臨界區的鎖,而此時這個鎖正被線程B所持有,那麼線程A就會被阻塞 (blocking),Core0 會在此時進行上下文切換(Context Switch)將線程A置於等待隊列中,此時Core0就能夠運行其餘的任務(例如另外一個線程C)而沒必要進行忙等待。而自旋鎖則否則,它屬於busy-waiting類型的鎖,若是線程A是使用pthread_spin_lock操做去請求鎖,那麼線程A就會一直在 Core0上進行忙等待並不停的進行鎖請求,直到獲得這個鎖爲止。
  • 兩種鎖的區別 互斥鎖的起始原始開銷要高於自旋鎖,可是基本是一勞永逸,臨界區持鎖時間的大小並不會對互斥鎖的開銷形成影響,而自旋鎖是死循環檢測,加鎖全程消耗cpu,起始開銷雖然低於互斥鎖,可是隨着持鎖時間,加鎖的開銷是線性增加。
  • 兩種鎖的應用 互斥鎖用於臨界區持鎖時間比較長的操做,好比下面這些狀況均可以考慮
  1. 臨界區有IO操做
  2. 臨界區代碼複雜或者循環量大
  3. 臨界區競爭很是激烈
  4. 單核處理器

至於自旋鎖就主要用在臨界區持鎖時間很是短且CPU資源不緊張的狀況下,自旋鎖通常用於多核的服務器。

2.8 公平鎖和非公平鎖的區別

非公平鎖:在等待鎖的過程當中,若是有人以新的線程妄圖獲取鎖,都是有很大概率直接獲取到鎖的。白話文:公平鎖是先到先得,按序進行,非公平鎖就是不排隊直接拿,失敗再說。

3 CAS

Compare and Swap,即比較再交換。jdk5增長了併發包java.util.concurrent.*,其下面的類使用CAS算法實現了區別於synchronouse同步鎖的一種樂觀鎖。JDK 5以前Java語言是靠synchronized關鍵字保證同步的,這是一種獨佔鎖,也是是悲觀鎖。

  • CAS算法理解

與鎖相比,使用比較交換(下文簡稱CAS)會使程序看起來更加複雜一些。但因爲其非阻塞性,它對死鎖問題天生免疫,而且,線程間的相互影響也遠遠比基於鎖的方式要小。更爲重要的是,使用無鎖的方式徹底沒有鎖競爭帶來的系統開銷,也沒有線程間頻繁調度帶來的開銷,所以,它要比基於鎖的方式擁有更優越的性能。

  • 無鎖的好處:
  1. 在高併發的狀況下,它比有鎖的程序擁有更好的性能;
  2. 它天生就是死鎖免疫的。
  • 優勢 效率比較高,無阻塞,無等待,重試
  • 缺點:
  1. 會產生ABA問題:由於CAS須要在操做值的時候,檢查值有沒有發生變化,若是沒有變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼CAS檢查時發現它的值沒有發生變化,但實際上發生了變化:A->B->A的過程
  2. 循環時間長,開銷大:自旋CAS若是長時間不成功,會給CPU帶來很大的執行開銷
  3. 只能保證一個共享變量的原子操做:當對一個共享變量操做時,咱們能夠採用CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性

4 原子類

java.util.concurrent.atomic包:原子類的小工具包,支持在單個變量上解除鎖的線程安全編程原子變量類至關於一種泛化的 volatile 變量,可以支持原子的和有條件的讀-改-寫操做。AtomicInteger 表示一個int類型的值,並提供了 get 和 set 方法,這些 Volatile 類型的int變量在讀取和寫入上有着相同的內存語義。它還提供了一個原子的 compareAndSet 方法(若是該方法成功執行,那麼將實現與讀取/寫入一個 volatile 變量相同的內存效果),以及原子的添加、遞增和遞減等方法。AtomicInteger 表面上很是像一個擴展的 Counter 類,但在發生競爭的狀況下能提供更高的可伸縮性,由於它直接利用了硬件對併發的支持。

若是同一個變量要被多個線程訪問,則可使用該包中的類 AtomicBoolean AtomicInteger AtomicLong AtomicReference

相關文章
相關標籤/搜索