Lock是一個相似同步代碼塊(synchronized block)的線程同步機制。同步代碼塊而言,Lock能夠作到更細粒度的控制。 Lock(或者其餘高級同步機制)也是基於同步代碼塊(synchronized block),因此還不能徹底摒棄
synchronized
關鍵字。html
從Java 5開始,
java.util.concurrent.locks
包提供了幾個Lock的實現類。你能夠直接使用,前提是你知道如何使用它們,並且只有瞭解它們的內部機制,才能更好的使用它們。 更多的內容能夠參考原做者的java.util.concurrent.locks.Lock相關文章,以及JDK API。java
先來看看同步塊的代碼:安全
public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } } }
注意
inc()
方法中的synchronized(this)
塊。 這個代碼塊,能夠確保同一時間,只有一個線程能夠執行return ++count
。 固然,同步塊還有不少高級的用法,這裏只是簡單的用於保障++count
的安全。併發
對於上面的
Counter
類,能夠用Lock
進行改寫:this
public class Counter{ private Lock lock = new Lock(); private int count = 0; public int inc(){ lock.lock(); int newCount = ++count; lock.unlock(); return newCount; } }
Lock的
lock()
會讓沒有獲得鎖的線程進入等待,直到unlock()
方法被調用。.net
下面看看一個簡單的Lock實現:線程
public class Lock{ private boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } public synchronized void unlock(){ isLocked = false; notify(); } }
注意:,這裏使用了一個
while(isLocked)
循環,起到了一個「自旋鎖(spin lock)」的效果。 有關自旋鎖以及wait()
和notify()
的信息,能夠去看看《線程間的信號處理》code
當
isLocked
爲true時,若是有線程調用lock()
方法,那就會進入wait()
等待。 此時,線程可能會被意外喚醒,從而退出wait()
方法,因此,須要用循環來再次檢查isLocked
的條件。(這就是:僞喚醒)htm
若是,
isLocked
爲false,則線程不會進入while(isLocked)
等待,而是會直接修改isLocked
爲true,從而達到加鎖的效果。對象
當線程完成了臨界區的代碼,而且調用了
unlock()
。接着將isLocked
條件置回false。而且經過notify()
方法,喚醒等待線程,從而達到釋放鎖的效果。
Java中的同步塊是能夠重入的(Reentrance)。 這就意味着,Java線程進入一個同步代碼塊,就能夠得到對象上的監控器鎖(monitor),當這個線程去訪問同一個監控器鎖保護的同步代碼塊時,就能夠直接進入了。
來看看這個例子:
public class Reentrant{ public synchronized outer(){ inner(); } public synchronized inner(){ //do something } }
注意,
outer()
和inner()
都聲明爲synchronized
,這在Java中,就等價於synchronized(this)
。 若是一個線程在outer()
方法中調用了inner()
方法。那麼,因爲這兩個方法,實際都是由同一個監控器(monitor)管理的(this),因此,能夠直接進入inner()
方法。
若是一個線程已經持有了某個監控器,那麼它就能夠訪問這個監控器保護的全部同步代碼塊。而這種特性就稱之爲「重入」,線程能夠從新進入已經得到的鎖所保護的代碼塊。
以前,展現的Lock實現,是一個不可重入鎖。 而若是,咱們重寫一個像下面這樣的
Reentrant
實現,那麼當線程調用outer()
方法將會被inner()
內的lock.lock()
阻塞住。
public class Reentrant2{ Lock lock = new Lock(); public outer(){ lock.lock(); inner(); lock.unlock(); } public synchronized inner(){ lock.lock(); //do something lock.unlock(); } }
當某個線程調用
outer()
方法,會首先鎖住Lock實例。 接着,調用inner()
方法。而inner()
會再次對Lock實例進行加鎖。 因爲Lock實例已經在out()
方法中加鎖了,因此這裏就會失敗,並且會致使線程一直阻塞下去。
線程第二次調用
lock()
方法期間沒有調用unlock()
從而致使上述問題。 因此,咱們再來看看lock()
方法的實現:
public class Lock{ boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } ... }
內部循環的條件,決定着是否容許退出
lock()
方法。 這裏僅僅是判斷是否處於加鎖狀態,而不關心是哪一個線程持有着鎖。
因此,須要對Lock作一些改造:
public class Lock{ boolean isLocked = false; Thread lockedBy = null; int lockedCount = 0; public synchronized void lock() throws InterruptedException{ Thread callingThread = Thread.currentThread(); while(isLocked && lockedBy != callingThread){ wait(); } isLocked = true; lockedCount++; lockedBy = callingThread; } public synchronized void unlock(){ if(Thread.curentThread() == this.lockedBy){ lockedCount--; if(lockedCount == 0){ isLocked = false; notify(); } } } ... }
如今,自旋循環的條件能夠直接放行已經持有鎖的線程了。 若是鎖是自由狀態(
isLocked = false
),或者執行線程就是持有鎖的線程,循環都不會執行,線程能夠順利的退出lock()
方法。
另外,咱們須要記錄同一個線程加鎖的次數。 由於,在
unlock()
釋放鎖時,咱們須要知道多少次後才能真正釋放鎖。 而這也將決定,unlock()
須要執行與lock()
對應的次數,才能釋放鎖。
如今,Lock就是一個可重入鎖了。
Java的
synchronized
是不保障線程進入的順序的。 所以,若是有多個線程不斷的競爭同一個同步塊,那就有可能某些線程永遠也沒法獲得訪問權(比較悲催的線程每次都沒有被喚醒)。 這就造成了飢餓。 爲了不這個問題,就須要把Lock實現爲公平性的。 因爲這裏的Lock都是基於synchronized
的,因此就沒法保障公平性。 更多有關公平性的問題,能夠參見:《併發中的飢餓問題以及公平性》
使用Lock來保護臨界區時,可能會因爲異常,致使沒有機會執行
unlock()
方法。 因此,須要經過finally
來釋放鎖,這樣才能確保安全。
lock.lock(); try{ //do critical section code, which may throw exception } finally { lock.unlock(); }
這樣一個範式,能夠確保Lock能夠獲得有效釋放。
不論對於什麼類型的Lock,都須要考慮幾點: