Java鎖及實現方式

鎖的概念在數據庫出現比較多,爲了實現數據庫的不同隔離級別,數據庫會定義不同的鎖類型。Java爲了實現同步及線程安全,也會定義不同的鎖。所謂的同步操作即原子操作(atomic operation)意爲“不可被中斷的一個或一系列操作”,類似數據庫中的事務。

線程安全實現方式

互斥同步(鎖機制)

互斥是實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和信號量(Semaphore)都是主要的互斥實現方式。

Java主要實現方式:synchronized和ReentrantLock

ReentrantLock 實現等待可中斷、 可實現公平鎖, 以及鎖可以綁定多個條件。

等待可中斷是指當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改爲處理其他事情,可中斷特性對處理執行時間非常長的同步塊很有幫助。

公平鎖是指多個線程在等待同一個鎖時, 必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖則不保證這一點,在鎖被釋放時, 任何一個等待鎖的線程都有機會獲得鎖。synchronized中的鎖是非公平的,ReentrantLock默認情況下也是非公平的, 但可以通過帶布爾值的構造函數要求使用公平鎖。

鎖綁定多個條件是指一個ReentrantLock對象可以同時綁定多個Condition對象,而在synchronized中,鎖對象的wait()和notify()或notifyAll() 方法可以實現一個隱含的條件, 如果要和多於一個的條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock則無須這樣做,只需要多次調用newCondition() 方法即可。

非阻塞同步(使用循環CAS實現原子操作)

基於衝突檢測的樂觀併發策略,通俗地說,就是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果共享數據有爭用,產生了衝突,那就再採取其他的補償措施(最常見的補償措施就是不斷地重試,直到成功爲止),這種樂觀的併發策略的許多實現都不需要把線程掛起,因此這種同步操作稱爲非阻塞同步(Non-Blocking Synchronization)。

因爲我們需要操作和衝突檢測這兩個步驟具備原子性,靠什麼來保證呢? 如果這裏再使用互斥同步來保證就失去意義了,所以我們只能靠硬件來完成這件事情,硬件保證一個從語義上看起來需要多次操作的行爲只通過一條處理器指令就能完成,這類指令常用的有:

  • 測試並設置(Test-and-Set)
  • 獲取並增加(Fetch-and-Increment)
  • 交換(Swap)
  • 比較並交換(Compare-and-Swap,下文稱CAS)
  • 加載鏈接/條件存儲(Load-Linked/Store-Conditional,下文稱LL/SC)

由於Unsafe類不是提供給用戶程序調用的類(Unsafe.getUnsafe(的代碼中限制了只有啓動類加載器(Bootstrap ClassLoader) 加載的Class才能訪問它),因此,如果不採用反射手段,我們只能通過其他的Java API來間接使用它,如J.U.C包裏面的整數原子類,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe類的CAS操作。

無同步方案

可重入代碼(Reentrant Code):這種代碼也叫做純代碼(Pure Code),可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼(包括遞歸調用它本身),而在控制權返回後,原來的程序不會出現任何錯誤。相對線程安全來說,可重入性是更基本的特性,它可以保證線程安全,即所有的可重入的代碼都是線程安全的,但是並非所有的線程安全的代碼都是可重入的。

可重入代碼有一些共同的特徵, 例如不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都由參數中傳入、不調用非可重入的方法等。我們可以通過一個簡單的原則來判斷代碼是否具備可重入性:如果一個方法,它的返回結果是可以預測的,只要輸入了相同的數據,就都能返回相同的結果,那它就滿足可重入性的要求,當然也就是線程安全的。

線程本地存儲(Thread Local Storage):如果一段代碼中所需要的數據必須與其他代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行?如果能保證,我們就可以把共享數據的可見範圍限制在同一個線程之內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。

Java 鎖

隊列同步器 AbstractQueuedSynchronizer

隊列同步器AbstractQueuedSynchronizer(以下簡稱同步器),是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。

同步器是實現鎖(也可以是任意同步組件)的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。

同步器的設計是基於模板方法模式的,也就是說,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。

重寫同步器指定的方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態。

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

隊列同步器的實現分析

同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成爲一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。

獨佔式同步狀態獲取與釋放

通過調用同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是由於線程獲取同步狀態失敗後進入同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移出。

共享式同步狀態獲取與釋放

共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態。以文件的讀寫爲例,如果一個程序在對文件進行讀操作,那麼這一時刻對於該文件的寫操作均被阻塞,而讀操作能夠同時進行。寫操作要求對資源的獨佔式訪問,而讀操作可以是共享式訪問。

獨佔式超時獲取同步狀態

通過調用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則,返回false。該方法提供了傳統Java同步操作(比如synchronized關鍵字)所不具備的特性。

自定義同步組件——TwinsLock

首先,確定訪問模式。TwinsLock能夠在同一時刻支持多個線程的訪問,這顯然是共享式訪問,因此,需要使用同步器提供的acquireShared(int args)方法等和Shared相關的方法,這就要求TwinsLock必須重寫tryAcquireShared(int args)方法和tryReleaseShared(int args)方法,這樣才能保證同步器的共享式同步狀態的獲取與釋放方法得以執行。

其次,定義資源數。TwinsLock在同一時刻允許至多兩個線程的同時訪問,表明同步資源數爲2,這樣可以設置初始狀態status爲2,當一個線程進行獲取,status減1,該線程釋放,則status加1,狀態的合法範圍爲0、1和2,其中0表示當前已經有兩個線程獲取了同步資源,此時再有其他線程對同步狀態進行獲取,該線程只能被阻塞。在同步狀態變更時,需要使用compareAndSet(int expect,int update)方法做原子性保障。

public class TwinsLock implements Lock {
	private final Sync sync = new Sync(2);

	private static final class Sync extends AbstractQueuedSynchronizer {
		Sync(int count) {
			if (count <= 0) {
				throw new IllegalArgumentException("count must large than zero.");
			}
			setState(count);
		}

		public int tryAcquireShared(int reduceCount) {
			for (;;) {
				int current = getState();
				int newCount = current - reduceCount;
				if (newCount < 0 || compareAndSetState(current, newCount)) {
					return newCount;
				}
			}
		}

		public boolean tryReleaseShared(int returnCount) {
			for (;;) {
				int current = getState();
				int newCount = current + returnCount;
				if (compareAndSetState(current, newCount)) {
					return true;
				}
			}
		}
	}

	public void lock() {
		sync.acquireShared(1);
	}

	public void unlock() {
		sync.releaseShared(1);
	}
// 其他接口方法略
	@Override
	public void lockInterruptibly() throws InterruptedException {
		// TODO Auto-generated method stub
	}
	@Override
	public boolean tryLock() {
		// TODO Auto-generated method stub
		return false;
	}
	@Override
	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		// TODO Auto-generated method stub
		return false;
	}
	@Override
	public Condition newCondition() {
		// TODO Auto-generated method stub
		return null;
	}
}

重入鎖 ReentrantLock

重進入是指任意線程在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞,該特性的實現需要解決以下兩個問題。

  1. 線程再次獲取鎖。鎖需要去識別獲取鎖的線程是否爲當前佔據鎖的線程,如果是,則再次成功獲取。
  2. 鎖的最終釋放。線程重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他線程能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放。

讀寫鎖 ReentrantReadWriteLock

在沒有讀寫鎖支持的時候,如果需要完成上述工作就要使用Java的等待通知機制,就是當寫操作開始時,所有晚於寫操作的讀操作均會進入等待狀態,只有寫操作完成並進行通知之後,所有等待的讀操作才能繼續執行(寫操作之間依靠synchronized關鍵進行同步),這樣做的目的是使讀操作能讀取到正確的數據,不會出現髒讀。改用讀寫鎖實現上述功能,只需要在讀操作時獲取讀鎖,寫操作時獲取寫鎖即可。當寫鎖被獲取到時,後續(非當前寫操作線程)的讀寫操作都會被阻塞,寫鎖釋放之後,所有操作繼續執行,編程方式相對於使用等待通知機制的實現方式而言,變得簡單明瞭。

讀寫鎖的性能都會比排它鎖好,因爲大多數場景讀是多於寫的。

LockSupport工具

當需要阻塞或喚醒一個線程的時候,都會使用LockSupport工具類來完成相應工作。LockSupport定義了一組的公共靜態方法,這些方法提供了最基本的線程阻塞和喚醒功能,而LockSupport也成爲構建同步組件的基礎工具。

Condition接口

任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有差別的。

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
    lock.lock();
    try {
        condition.await();
    } finally {
        lock.unlock();
    }
}
public void conditionSignal() throws InterruptedException {
    lock.lock();
    try {
        condition.signal();
    } finally {
        lock.unlock();
    }
}

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章