併發程式設計(三)—— ReentrantLock實現原理及原始碼分析
ReentrantLock是Java併發包中提供的一個可重入的互斥鎖。ReentrantLock和synchronized在基本用法,行為語義上都是類似的,同樣都具有可重入性。只不過相比原生的Synchronized,ReentrantLock增加了一些高階的擴充套件功能,比如它可以實現公平鎖,同時也可以繫結多個Conditon。
可重入性/公平鎖/非公平鎖
可重入性
所謂的可重入性,就是可以支援一個執行緒對鎖的重複獲取,原生的synchronized就具有可重入性,一個用synchronized修飾的遞迴方法,當執行緒在執行期間,它是可以反覆獲取到鎖的,而不會出現自己把自己鎖死的情況。ReentrantLock也是如此,在呼叫lock()方法時,已經獲取到鎖的執行緒,能夠再次呼叫lock()方法獲取鎖而不被阻塞。
公平鎖/非公平鎖
所謂公平鎖,顧名思義,意指鎖的獲取策略相對公平,當多個執行緒在獲取同一個鎖時,必須按照鎖的申請時間來依次獲得鎖,排排隊,不能插隊;非公平鎖則不同,當鎖被釋放時,等待中的執行緒均有機會獲得鎖。synchronized是非公平鎖,ReentrantLock預設也是非公平的,但是可以通過帶boolean引數的構造方法指定使用公平鎖,但非公平鎖的效能一般要優於公平鎖。
synchronized是Java原生的互斥同步鎖,使用方便,對於synchronized修飾的方法或同步塊,無需再顯式釋放鎖。而ReentrantLock做為API層面的互斥鎖,需要顯式地去加鎖解鎖。採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。
class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // 加鎖 try { // ... 函式主題 } finally { lock.unlock() //解鎖 } } }
原始碼分析
接下來我們從原始碼角度來看看ReentrantLock的實現原理,它是如何保證可重入性,又是如何實現公平鎖的。
1、無參構造器(預設為非公平鎖)
public ReentrantLock() { sync = new NonfairSync();//預設是非公平的 }
sync是ReentrantLock內部實現的一個同步元件,它是Reentrantlock的一個靜態內部類,繼承於AQS。
2、帶布林值的構造器(是否公平)
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();//fair為true,公平鎖;反之,非公平鎖 }
此處可以指定是否採用公平鎖,FailSync和NonFailSync亦為Reentrantlock的靜態內部類,都繼承於Sync。
3、lock()
public void lock() { sync.lock();//代理到Sync的lock方法上 }
Sync的lock方法是抽象的,實際的lock會代理到FairSync或是NonFairSync上(根據使用者的選擇來決定,公平鎖還是非公平鎖)
4、unlock()
public void unlock() { sync.release(1);//釋放鎖 }
釋放鎖,呼叫sync的release方法。
5、tryLock()
Lock lock = ...; if(lock.tryLock()) { try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 } }else { //如果不能獲取鎖,則直接做其他事情 }
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false。
6、newCondition()
public Condition newCondition() { return sync.newCondition(); }
獲取一個conditon,ReentrantLock支援多個Condition
7、await()
public class MyService { private Lock lock = new ReentrantLock(); private Condition condition=lock.newCondition(); public void testMethod() { try { lock.lock(); System.out.println("開始wait"); condition.await(); for (int i = 0; i < 5; i++) { System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1))); } } catch (InterruptedException e) { // TODO 自動生成的 catch 塊 e.printStackTrace(); } finally { lock.unlock(); } } }
通過建立Condition物件來使執行緒wait,必須先執行lock.lock方法獲得鎖
8、signal()
public void signal() { try { lock.lock(); condition.signal(); } finally { lock.unlock(); } }
condition物件的signal方法可以喚醒wait執行緒
9、建立多個condition物件
一個condition物件的signal(signalAll)方法和該物件的await方法是一一對應的,也就是一個condition物件的signal(signalAll)方法不能喚醒其他condition物件的await方法
ABC迴圈列印20遍
1 package main.java.Juc; 2 3 import java.util.concurrent.locks.Condition; 4 import java.util.concurrent.locks.Lock; 5 import java.util.concurrent.locks.ReentrantLock; 6 7 /* 8 * 編寫一個程式,開啟 3 個執行緒,這三個執行緒的 ID 分別為 A、B、C,每個執行緒將自己的 ID 在螢幕上列印 10 遍,要求輸出的結果必須按順序顯示。 9 * 如:ABCABCABC…… 依次遞迴 10 */ 11 public class TestABCAlternate { 12 13 public static void main(String[] args) { 14 AlternateDemo ad = new AlternateDemo(); 15 16 new Thread(new Runnable() { 17 @Override 18 public void run() { 19 for (int i = 1; i <= 20; i++) { 20 ad.loopA(i); 21 } 22 } 23 }, "A").start(); 24 25 new Thread(new Runnable() { 26 @Override 27 public void run() { 28 for (int i = 1; i <= 20; i++) { 29 ad.loopB(i); 30 } 31 } 32 }, "B").start(); 33 34 new Thread(new Runnable() { 35 @Override 36 public void run() { 37 for (int i = 1; i <= 20; i++) { 38 ad.loopC(i); 39 System.out.println("-----------------------------------"); 40 } 41 } 42 }, "C").start(); 43 } 44 45 } 46 47 class AlternateDemo{ 48 49 private int number = 1; //當前正在執行執行緒的標記 50 51 private Lock lock = new ReentrantLock(); 52 private Condition condition1 = lock.newCondition(); 53 private Condition condition2 = lock.newCondition(); 54 private Condition condition3 = lock.newCondition(); 55 56 /** 57 * @param totalLoop : 迴圈第幾輪 58 */ 59 public void loopA(int totalLoop){ 60 lock.lock(); 61 try { 62 //1. 判斷 63 if(number != 1){ 64 condition1.await(); 65 } 66 //2. 列印 67 for (int i = 1; i <= 1; i++) { 68 System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop); 69 } 70 //3. 喚醒 71 number = 2; 72 condition2.signal(); 73 } catch (Exception e) { 74 e.printStackTrace(); 75 } finally { 76 lock.unlock(); 77 } 78 } 79 80 public void loopB(int totalLoop){ 81 lock.lock(); 82 try { 83 //1. 判斷 84 if(number != 2){ 85 condition2.await(); 86 } 87 //2. 列印 88 for (int i = 1; i <= 1; i++) { 89 System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop); 90 } 91 //3. 喚醒 92 number = 3; 93 condition3.signal(); 94 } catch (Exception e) { 95 e.printStackTrace(); 96 } finally { 97 lock.unlock(); 98 } 99 } 100 101 public void loopC(int totalLoop){ 102 lock.lock(); 103 try { 104 //1. 判斷 105 if(number != 3){ 106 condition3.await(); 107 } 108 //2. 列印 109 for (int i = 1; i <= 1; i++) { 110 System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop); 111 } 112 //3. 喚醒 113 number = 1; 114 condition1.signal(); 115 } catch (Exception e) { 116 e.printStackTrace(); 117 } finally { 118 lock.unlock(); 119 } 120 } 121 122 }
執行結果:
程式碼分析:
三個執行緒分別迴圈20次呼叫loopA、loopB、loopC列印,但是不確定是哪個方法先被呼叫到,如果是loopB先呼叫,則loopB方法先獲取到鎖,loopA和loopC等待鎖,此時執行緒執行標記number=1,程式碼84行處為true,則condition2.await();如果需要喚醒此執行緒,則需要用condition2來喚醒,此時執行緒交出鎖;
如果loopA獲取了鎖,loopB和loopC等待鎖,此時執行緒執行標記number=1,程式碼63行處為false,則執行67行列印,列印完則用condition2.signal()喚醒列印loopB的執行緒,接著loopB的執行緒去列印B,執行緒loopB列印完畢去喚醒列印loopC的執行緒,列印完loopC再喚醒loopA,如此迴圈20次。
總結
1、Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內建的語言實現;
2、synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
3、Lock類可以建立Condition物件,Condition物件用來是執行緒等待和喚醒執行緒,需要注意的是Condition物件的喚醒的是用同一個Condition執行await方法的執行緒,所以也就可以實現喚醒指定類的執行緒