Java中的顯示鎖ReentrantLock使用與原理
考慮一個場景,輪流列印0-100以內的技術和偶數。通過使用 synchronize 的 wait,notify機制就可以實現,核心思路如下:
使用兩個執行緒,一個列印奇數,一個列印偶數。這兩個執行緒會共享一個數據,資料每次自增,當列印奇數的執行緒發現當前要列印的數字不是奇數時,執行等待,否則列印奇數,並將數字自增1,對於列印偶數的執行緒也是如此
//列印奇數的執行緒 private static class OldRunner implements Runnable{ private MyNumber n; public OldRunner(MyNumber n) { this.n = n; } public void run() { while (true){ n.waitToOld(); //等待資料變成奇數 System.out.println("old:" + n.getVal()); n.increase(); if (n.getVal()>98){ break; } } } } //列印偶數的執行緒 private static class EvenRunner implements Runnable{ private MyNumber n; public EvenRunner(MyNumber n) { this.n = n; } public void run() { while (true){ n.waitToEven(); //等待資料變成偶數 System.out.println("even:"+n.getVal()); n.increase(); if (n.getVal()>99){ break; } } } }
共享的資料如下
private static class MyNumber{ private int val; public MyNumber(int val) { this.val = val; } public int getVal() { return val; } public synchronized void increase(){ val++; notify(); //資料變了,喚醒另外的執行緒 } public synchronized void waitToOld(){ while ((val % 2)==0){ try { System.out.println("i am "+Thread.currentThread().getName()+" ,but now is even:"+val+",so wait"); wait(); //只要是偶數,一直等待 } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void waitToEven(){ while ((val % 2)!=0){ try { System.out.println("i am "+Thread.currentThread().getName()+" ,but now old:"+val+",so wait"); wait(); //只要是奇數,一直等待 } catch (InterruptedException e) { e.printStackTrace(); } } } }
執行程式碼如下
MyNumber n = new MyNumber(0);
Thread old=new Thread(new OldRunner(n),"old-thread");
Thread even = new Thread(new EvenRunner(n),"even-thread");
old.start();
even.start();
執行結果如下
i am old-thread ,but now is even:0,so wait even:0 i am even-thread ,but now old:1,so wait old:1 i am old-thread ,but now is even:2,so wait even:2 i am even-thread ,but now old:3,so wait old:3 i am old-thread ,but now is even:4,so wait even:4 i am even-thread ,but now old:5,so wait old:5 i am old-thread ,but now is even:6,so wait even:6 i am even-thread ,but now old:7,so wait old:7 i am old-thread ,but now is even:8,so wait even:8
上述方法使用的是 synchronize的 wait notify機制,同樣可以使用顯示鎖來實現,兩個列印的執行緒還是同一個執行緒,只是使用的是顯示鎖來控制等待事件
private static class MyNumber{
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private int val;
public MyNumber(int val) {
this.val = val;
}
public int getVal() {
return val;
}
public void increase(){
lock.lock();
try {
val++;
condition.signalAll(); //通知執行緒
}finally {
lock.unlock();
}
}
public void waitToOld(){
lock.lock();
try{
while ((val % 2)==0){
try {
System.out.println("i am should print old ,but now is even:"+val+",so wait");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
}
public void waitToEven(){
lock.lock(); //顯示的鎖定
try{
while ((val % 2)!=0){
try {
System.out.println("i am should print even ,but now old:"+val+",so wait");
condition.await();//執行等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock(); //顯示的釋放
}
}
}
同樣可以得到上述的效果
顯示鎖的功能
顯示鎖在java中通過介面Lock提供如下功能
- lock: 執行緒無法獲取鎖會進入休眠狀態,直到獲取成功
- lockInterruptibly: 如果獲取成功,立即返回,否則一直休眠到執行緒被中斷或者是獲取成功
- tryLock:不會造成執行緒休眠,方法執行會立即返回,獲取到了鎖,返回true,否則返回false
- tryLock(long time, TimeUnit unit) throws InterruptedException : 在等待時間內沒有發生過中斷,並且沒有獲取鎖,就一直等待,當獲取到了,或者是執行緒中斷了,或者是超時時間到了這三者發生一個就返回,並記錄是否有獲取到鎖
- unlock:釋放鎖
- newCondition:每次呼叫建立一個鎖的等待條件,也就是說一個鎖可以擁有多個條件
Condition的功能
介面Condition把Object的監視器方法wait和notify分離出來,使得一個物件可以有多個等待的條件來執行等待,配合Lock的newCondition來實現。
- await:使當前執行緒休眠,不可排程。這四種情況下會恢復 1:其它執行緒呼叫了signal,當前執行緒恰好被選中了恢復執行;2: 其它執行緒呼叫了signalAll;3:其它執行緒中斷了當前執行緒 4:spurious wakeup (假醒)。無論什麼情況,在await方法返回之前,當前執行緒必須重新獲取鎖
- awaitUninterruptibly:使當前執行緒休眠,不可排程。這三種情況下會恢復 1:其它執行緒呼叫了signal,當前執行緒恰好被選中了恢復執行;2: 其它執行緒呼叫了signalAll;3:spurious wakeup (假醒)。
- awaitNanos:使當前執行緒休眠,不可排程。這四種情況下會恢復 1:其它執行緒呼叫了signal,當前執行緒恰好被選中了恢復執行;2: 其它執行緒呼叫了signalAll;3:其它執行緒中斷了當前執行緒 4:spurious wakeup (假醒)。5:超時了
- await(long time, TimeUnit unit) :與awaitNanos類似,只是換了個時間單位
- awaitUntil(Date deadline):與awaitNanos相似,只是指定日期之後返回,而不是指定的一段時間
- signal:喚醒一個等待的執行緒
- signalAll:喚醒所有等待的執行緒
ReentrantLock
從原始碼中可以看到,ReentrantLock的所有實現全都依賴於內部類Sync和ConditionObject。
Sync本身是個抽象類,負責手動lock和unlock,ConditionObject則實現在父類AbstractOwnableSynchronizer中,負責await與signal
Sync的繼承結構如下
Sync的兩個實現類,公平鎖和非公平鎖
公平的鎖會把許可權給等待時間最長的執行緒來執行,非公平則獲取執行許可權的執行緒與執行緒本身的等待時間無關
預設初始化ReentrantLock使用的是非公平鎖,當然可以通過指定引數來使用公平鎖
public ReentrantLock() { sync = new NonfairSync(); }
當執行獲取鎖時,實際就是去執行 Sync 的lock操作:
public void lock() {
sync.lock();
}
對應在不同的鎖機制中有不同的實現
- 公平鎖實現
final void lock() { acquire(1); }
- 非公平鎖實現
final void lock() { if (compareAndSetState(0, 1)) //先看當前鎖是不是已經被佔有了,如果沒有,就直接將當前執行緒設定為佔有的執行緒 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); //鎖已經被佔有的情況下,嘗試獲取 }
二者都呼叫父類AbstractQueuedSynchronizer的方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //一旦搶失敗,就會進入佇列,進入佇列後則是依據FIFO的原則來執行喚醒
selfInterrupt();
}
當執行unlock時,對應方法在父類AbstractQueuedSynchronizer中
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
公平鎖和非公平鎖則分別對獲取鎖的方式tryAcquire
做了實現,而tryRelease的實現機制則都是一樣的
公平鎖實現tryAcquire
原始碼如下
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); //獲取當前的同步狀態
if (c == 0) {
//等於0 表示沒有被其它執行緒獲取過鎖
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//hasQueuedPredecessors 判斷在當前執行緒的前面是不是還有其它的執行緒,如果有,也就是鎖sync上有一個等待的執行緒,那麼它不能獲取鎖,這意味著,只有等待時間最長的執行緒能夠獲取鎖,這就是是公平性的體現
//compareAndSetState 看當前在記憶體中儲存的值是不是真的是0,如果是0就設定成accquires的取值。對於JAVA,這種需要直接操作記憶體的操作是通過unsafe來完成,具體的實現機制則依賴於作業系統。
//儲存獲取當前鎖的執行緒
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//判斷是不是當前執行緒獲取的鎖
int nextc = c + acquires;
if (nextc < 0)//一個執行緒能夠獲取同一個鎖的次數是有限制的,就是int的最大值
throw new Error("Maximum lock count exceeded");
setState(nextc); //在當前的基礎上再增加一次鎖被持有的次數
return true;
}
//鎖被其它執行緒持有,獲取失敗
return false;
}
非公平鎖實現tryAcquire
獲取的關鍵實現為nonfairTryAcquire
,原始碼如下
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//鎖沒有被持有
//可以看到這裡會無視sync queue中是否有其它執行緒,只要執行到了當前執行緒,就會去獲取鎖
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); //在判斷一次是不是鎖沒有被佔有,沒有就去標記當前執行緒擁有這個鎖了
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//如果當前執行緒已經佔有過,增加佔有的次數
return true;
}
return false;
}
釋放鎖的機制
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) //只能是執行緒擁有這釋放
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//當佔有次數為0的時候,就認為所有的鎖都釋放完畢了
free = true;
setExclusiveOwnerThread(null);
}
setState(c); //更新鎖的狀態
return free;
}
從原始碼的實現可以看到
- ReentrantLock獲取鎖時,在鎖已經被佔有的情況下,如果佔有鎖的執行緒是當前執行緒,那麼允許重入,即再次佔有,如果由其它執行緒佔有,則獲取失敗,由此可見,
ReetrantLock本身對鎖的持有是可重入的,同時是執行緒獨佔的
。 - 公平與非公平就體現在,當執行的執行緒去獲取鎖的時候,公平的會去看是否有等待時間比它更長的,而非公平的就優先直接去佔有鎖
ReentrantLock的tryLock()與tryLock(long timeout, TimeUnit unit):
public boolean tryLock() { //本質上就是執行一次非公平的搶鎖 return sync.nonfairTryAcquire(1); }
有時限的tryLock核心程式碼是
sync.tryAcquireNanos(1, unit.toNanos(timeout));
,由於有超時時間,它會直接放到等待佇列中,他與後面要講的AQS的lock原理中acquireQueued的區別在於park的時間是有限的,詳見原始碼AbstractQueuedSynchronizer.doAcquireNanos
為什麼需要顯示鎖
內建鎖功能上有一定的侷限性,它無法響應中斷,不能設定等待的時間