一:java.util.concurrent.locks包下常用的類與接口(lock是jdk 1.5后新增的)
(1)Lock和ReadWriteLock是兩大鎖的根接口,Lock代表實現類是ReentrantLock(可重入鎖),ReadWriteLock(讀寫鎖)的代表實現類是ReentrantReadWriteLock。
Lock 接口支持那些語義不同(重入、公平等)的鎖規則,可以在非阻塞式結構的上下文(包括 hand-over-hand 和鎖重排算法)中使用這些規則。主要的實現是 ReentrantLock。
ReadWriteLock 接口以類似方式定義了一些讀取者可以共享而寫入者獨占的鎖。此包只提供了一個實現,即 ReentrantReadWriteLock,因為它適用於大部分的標准用法上下文。但程序員可以創建自己的、適用於非標准要求的實現。
(2)Condition 接口描述了可能會與鎖有關聯的條件變量。這些變量在用法上與使用 Object.wait 訪問的隱式監視器類似,但提供了更強大的功能。需要特別指出的是,單個 Lock 可能與多個 Condition 對象關聯。為了避免兼容性問題,Condition 方法的名稱與對應的 Object 版本中的不同。
二:synchronized的缺陷
synchronized是java中的一個關鍵字,也就是說是Java語言內置的特性。那么為什么會出現Lock呢?
1)Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問;
2)Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之后,系統會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。
synchronized 的局限性 與 Lock 的優點
如果一個代碼塊被synchronized關鍵字修飾,當一個線程獲取了對應的鎖,並執行該代碼塊時,其他線程便只能一直等待直至占有鎖的線程釋放鎖。事實上,占有鎖的線程釋放鎖一般會是以下三種情況之一:
1:占有鎖的線程執行完了該代碼塊,然后釋放對鎖的占有;
2:占有鎖線程執行發生異常,此時JVM會讓線程自動釋放鎖;
3:占有鎖線程進入 WAITING 狀態從而釋放鎖,例如在該線程中調用wait()方法等。
試考慮以下三種情況:
Case 1 :
在使用synchronized關鍵字的情形下,假如占有鎖的線程由於要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,那么其他線程就只能一直等待,別無他法。這會極大影響程序執行效率。因此,就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間 (解決方案:tryLock(long time, TimeUnit unit)) 或者 能夠響應中斷 (解決方案:lockInterruptibly())),這種情況可以通過 Lock 解決。
Case 2 :
我們知道,當多個線程讀寫文件時,讀操作和寫操作會發生沖突現象,寫操作和寫操作也會發生沖突現象,但是讀操作和讀操作不會發生沖突現象。但是如果采用synchronized關鍵字實現同步的話,就會導致一個問題,即當多個線程都只是進行讀操作時,也只有一個線程在可以進行讀操作,其他線程只能等待鎖的釋放而無法進行讀操作。因此,需要一種機制來使得當多個線程都只是進行讀操作時,線程之間不會發生沖突。同樣地,Lock也可以解決這種情況 (解決方案:ReentrantReadWriteLock) 。
Case 3 :
我們可以通過Lock得知線程有沒有成功獲取到鎖 (解決方案:ReentrantLock) ,但這個是synchronized無法辦到的。
上面提到的三種情形,我們都可以通過Lock來解決,但 synchronized 關鍵字卻無能為力。事實上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 實現提供了比 synchronized 關鍵字 更廣泛的鎖操作,它能以更優雅的方式處理線程同步問題。也就是說,Lock提供了比synchronized更多的功能。
三:Lock接口實現類的使用
// 獲取鎖 void lock() // 如果當前線程未被中斷,則獲取鎖,可以響應中斷 void lockInterruptibly() // 返回綁定到此 Lock 實例的新 Condition 實例 Condition newCondition() // 僅在調用時鎖為空閑狀態才獲取該鎖,可以響應中斷 boolean tryLock() // 如果鎖在給定的等待時間內空閑,並且當前線程未被中斷,則獲取鎖 boolean tryLock(long time, TimeUnit unit) // 釋放鎖 void unlock()
下面來逐個分析Lock接口中每個方法。lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用來獲取鎖的。unLock()方法是用來釋放鎖的。newCondition() 返回 綁定到此 Lock 的新的 Condition 實例 ,用於線程間的協作,詳細內容請查找關鍵詞:線程間通信與協作。
1). lock()
在Lock中聲明了四個方法來獲取鎖,那么這四個方法有何區別呢?首先,lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。在前面已經講到,如果采用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此,一般來說,使用Lock必須在try…catch…塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:
Lock lock = ...; lock.lock(); try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 }
2). tryLock() & tryLock(long time, TimeUnit unit)
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true;如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就是說,這個方法無論如何都會立即返回(在拿不到鎖時不會一直在那等待)。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false,同時可以響應中斷。如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
一般情況下,通過tryLock來獲取鎖時是這樣使用的:
Lock lock = ...; if(lock.tryLock()) { try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 } }else { //如果不能獲取鎖,則直接做其他事情 }
3). lockInterruptibly()
lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程 正在等待獲取鎖,則這個線程能夠 響應中斷,即中斷線程的等待狀態。例如,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出 InterruptedException,但推薦使用后者,原因稍后闡述。因此,lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException { lock.lockInterruptibly(); try { //..... } finally { lock.unlock(); } }
注意,當一個線程獲取了鎖之后,是不會被interrupt()方法中斷的。因為interrupt()方法只能中斷阻塞過程中的線程而不能中斷正在運行過程中的線程。因此,當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,那么只有進行等待的情況下,才可以響應中斷的。與 synchronized 相比,當一個線程處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。
Lock的實現類 ReentrantLock
ReentrantLock,即 可重入鎖。ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法。下面通過一些實例學習如何使用 ReentrantLock。
構造方法(不帶參數 和帶參數 true: 公平鎖; false: 非公平鎖):
/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); } /** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockThread { Lock lock = new ReentrantLock(); public void lock(String name) { // 獲取鎖 lock.lock(); try { System.out.println(name + " get the lock"); // 訪問此鎖保護的資源 } finally { // 釋放鎖 lock.unlock(); System.out.println(name + " release the lock"); } } public static void main(String[] args) { LockThread lt = new LockThread(); new Thread(() -> lt.lock("A")).start(); new Thread(() -> lt.lock("B")).start(); } }
從執行結果可以看出,A線程和B線程同時對資源加鎖,A線程獲取鎖之后,B線程只好等待,直到A線程釋放鎖B線程才獲得鎖。
總結一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:
1)synchronized是Java語言的關鍵字,因此是內置特性,Lock不是Java語言內置的,Lock是一個接口,通過實現類可以實現同步訪問。
2)synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中
3)在資源競爭不是很激烈的情況下,Synchronized的性能要優於ReetrantLock,但是在資源競爭很激烈的情況下,Synchronized的性能會下降幾十倍,但是ReetrantLock的性能能維持常態。
ReadWriteLock鎖
//返回用於讀取操作的鎖 Lock readLock() //返回用於寫入操作的鎖 Lock writeLock()
ReadWriteLock 維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。只要沒有 writer,讀取鎖可以由多個 reader 線程同時保持,而寫入鎖是獨占的。
【例子】三個線程同時對一個共享數據進行讀寫
import java.util.Random; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; class Queue { //共享數據,只能有一個線程能寫該數據,但可以有多個線程同時讀該數據。 private Object data = null; ReadWriteLock lock = new ReentrantReadWriteLock(); // 讀數據 public void get() { // 加讀鎖 lock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " be ready to read data!"); Thread.sleep((long) (Math.random() * 1000)); System.out.println(Thread.currentThread().getName() + " have read data :" + data); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 釋放讀鎖 lock.readLock().unlock(); } } // 寫數據 public void put(Object data) { // 加寫鎖 lock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + " be ready to write data!"); Thread.sleep((long) (Math.random() * 1000)); this.data = data; System.out.println(Thread.currentThread().getName() + " have write data: " + data); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 釋放寫鎖 lock.writeLock().unlock(); } } } public class ReadWriteLockDemo { public static void main(String[] args) { final Queue queue = new Queue(); //一共啟動6個線程,3個讀線程,3個寫線程 for (int i = 0; i < 3; i++) { //啟動1個讀線程 new Thread() { public void run() { while (true) { queue.get(); } } }.start(); //啟動1個寫線程 new Thread() { public void run() { while (true) { queue.put(new Random().nextInt(10000)); } } }.start(); } } }
執行結果
四:鎖的相關概念介紹
1、可重入鎖如果鎖具備可重入性,則稱作為 可重入鎖 。像 synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明了 鎖的分配機制:基於線程的分配,而不是基於方法調用的分配。舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執行方法method2。
class MyClass { public synchronized void method1() { method2(); } public synchronized void method2() { } }
上述代碼中的兩個方法method1和method2都用synchronized修飾了。假如某一時刻,線程A執行到了method1,此時線程A獲取了這個對象的鎖,而由於method2也是synchronized方法,假如synchronized不具備可重入性,此時線程A需要重新申請鎖。但是,這就會造成死鎖,因為線程A已經持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。而由於synchronized和Lock都具備可重入性,所以不會發生上述現象。
如果某一線程A正在執行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由於等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。在前面演示tryLock(long time, TimeUnit unit)和lockInterruptibly()的用法時已經體現了Lock的可中斷性。
3、公平鎖
公平鎖即 盡量 以請求鎖的順序來獲取鎖。比如,同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。而非公平鎖則無法保證鎖的獲取是按照請求鎖的順序進行的,這樣就可能導致某個或者一些線程永遠獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。而對於ReentrantLock 和 ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設置為公平鎖