在Java中提供了synchronized關鍵字來保證只有一個線程可以訪問同步代碼塊。既然已經提供了synchronized關鍵字,那爲什麼在Java的SDK包中,還會提供Lock接口呢?這是否是重複造輪子,畫蛇添足呢?今天,咱們就一塊兒來探討下這個問題。
既然JVM中提供了synchronized關鍵字來保證只有一個線程可以訪問同步代碼塊,爲什麼還要提供Lock接口呢?這是在重複造輪子嗎?Java的設計者們爲什麼要這樣作呢?讓咱們一塊兒帶着疑問往下看。java
不少小夥伴可能會據說過,在Java 1.5版本中,synchronized的性能不如Lock,但在Java 1.6版本以後,synchronized作了不少優化,性能提高了很多。那既然synchronized關鍵字的性能已經提高了,那爲什麼還要使用Lock呢?函數
若是咱們向更深層次思考的話,就不難想到了:咱們使用synchronized加鎖是沒法主動釋放鎖的,這就會涉及到死鎖的問題。性能
若是要發生死鎖,則必須存在如下四個必要條件,四者缺一不可。優化
在一段時間內某資源僅爲一個線程所佔有。此時如有其餘線程請求該資源,則請求線程只能等待。this
線程所得到的資源在未使用完畢以前,不能被其餘線程強行奪走,即只能由得到該資源的線程本身來釋放(只能是主動釋放)。spa
線程已經保持了至少一個資源,但又提出了新的資源請求,而該資源已被其餘線程佔有,此時請求線程被阻塞,但對本身已得到的資源保持不放。線程
在發生死鎖時必然存在一個進程等待隊列{P1,P2,…,Pn},其中P1等待P2佔有的資源,P2等待P3佔有的資源,…,Pn等待P1佔有的資源,造成一個進程等待環路,環路中每個進程所佔有的資源同時被另外一個申請,也就是前一個進程佔有後一個進程所深情地資源。設計
若是咱們的程序使用synchronized關鍵字發生了死鎖時,synchronized關鍵是是沒法破壞「不可剝奪」這個死鎖的條件的。這是由於synchronized申請資源的時候, 若是申請不到, 線程直接進入阻塞狀態了, 而線程進入阻塞狀態, 啥都幹不了, 也釋放不了線程已經佔有的資源。code
然而,在大部分場景下,咱們都是但願「不可剝奪」這個條件可以被破壞。也就是說對於「不可剝奪」這個條件,佔用部分資源的線程進一步申請其餘資源時, 若是申請不到, 能夠主動釋放它佔有的資源, 這樣不可剝奪這個條件就破壞掉了。blog
若是咱們本身從新設計鎖來解決synchronized的問題,咱們該如何設計呢?
瞭解了synchronized的侷限性以後,若是是讓咱們本身實現一把同步鎖,咱們該如何設計呢?也就是說,咱們在設計鎖的時候,要如何解決synchronized的侷限性問題呢?這裏,我以爲能夠從三個方面來思考這個問題。
(1)可以響應中斷。 synchronized的問題是, 持有鎖A後, 若是嘗試獲取鎖B失敗, 那麼線程就進入阻塞狀態, 一旦發生死鎖, 就沒有任何機會來喚醒阻塞的線程。 但若是阻塞狀態的線程可以響應中斷信號, 也就是說當咱們給阻塞的線程發送中斷信號的時候, 可以喚醒它, 那它就有機會釋放曾經持有的鎖A。 這樣就破壞了不可剝奪條件了。
(2)支持超時。 若是線程在一段時間以內沒有獲取到鎖, 不是進入阻塞狀態, 而是返回一個錯誤, 那這個線程也有機會釋放曾經持有的鎖。 這樣也能破壞不可剝奪條件。
(3)非阻塞地獲取鎖。 若是嘗試獲取鎖失敗, 並不進入阻塞狀態, 而是直接返回, 那這個線程也有機會釋放曾經持有的鎖。 這樣也能破壞不可剝奪條件。
體如今Lock接口上,就是Lock接口提供的三個方法,以下所示。
`// 支持中斷的API void lockInterruptibly() throws InterruptedException; // 支持超時的API boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 支持非阻塞獲取鎖的API boolean tryLock();`
支持中斷。
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,若是獲取成功,則返回true,若是獲取失敗(即鎖已被其餘線程獲取),則返回false,也就說這個方法不管如何都會當即返回。在拿不到鎖時不會一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是相似的,只不過區別在於這個方法在拿不到鎖時會等待必定的時間,在時間期限以內若是還拿不到鎖,就返回false。若是一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
也就是說,對於死鎖問題,Lock可以破壞不可剝奪的條件,例如,咱們下面的程序代碼就破壞了死鎖的不可剝奪的條件。
`public class TansferAccount{ private Lock thisLock = new ReentrantLock(); private Lock targetLock = new ReentrantLock(); //帳戶的餘額 private Integer balance; //轉帳操做 public void transfer(TansferAccount target, Integer transferMoney){ boolean isThisLock = thisLock.tryLock(); if(isThisLock){ try{ boolean isTargetLock = targetLock.tryLock(); if(isTargetLock){ try{ if(this.balance >= transferMoney){ this.balance -= transferMoney; target.balance += transferMoney; } }finally{ targetLock.unlock } } }finally{ thisLock.unlock(); } } } }`
例外,Lock下面有一個ReentrantLock,而ReentrantLock支持公平鎖和非公平鎖。
在使用ReentrantLock的時候, ReentrantLock中有兩個構造函數, 一個是無參構造函數, 一個是傳入fair參數的構造函數。 fair參數表明的是鎖的公平策略, 若是傳入true就表示須要構造一個公平鎖, 反之則表示要構造一個非公平鎖。以下代碼片斷所示。
`//無參構造函數: 默認非公平鎖 public ReentrantLock() { sync = new NonfairSync(); } //根據公平策略參數建立鎖 public ReentrantLock(boolean fair){ sync = fair ? new FairSync() : new NonfairSync(); }`
鎖的實如今本質上都對應着一個入口等待隊列, 若是一個線程沒有得到鎖, 就會進入等待隊列, 當有線程釋放鎖的時候, 就須要從等待隊列中喚醒一個等待的線程。 若是是公平鎖, 喚醒的策略就是誰等待的時間長, 就喚醒誰, 很公平; 若是是非公平鎖, 則不提供這個公平保證, 有可能等待時間短的線程反而先被喚醒。 而Lock是支持公平鎖的,synchronized不支持公平鎖。
最後,值得注意的是,在使用Lock加鎖時,必定要在finally{}代碼塊中釋放鎖,例如,下面的代碼片斷所示。
`try{ lock.lock(); }finally{ lock.unlock(); }`
注:其餘synchronized和Lock的詳細說明,小夥伴們自行查閱便可。