ReentrantLock 與 synchronized對比
最近有在閱讀Java並發編程實戰這本書,又看到了ReentrantLock和synchronized的對比,發現自己以前對於RenntrantLock的理解很片面,特此做一番總結,如果有總結不到位的,歡迎指出
java.util.concurrent.locks
接口 Lock
為什么需要Lock?
java.util.concurrent.locks
接口 Lock
Lock提供了一種如條件的、可輪詢的、定時的以及可以終端的獲取鎖的操作,所有的加鎖方式和解鎖方式都是顯式的。
public class Lock { private boolean locked = false; public Lock() { } public final synchronized void lock() throws InterruptedException { while(this.locked) { this.wait(); } this.locked = true; } public final synchronized void unlock() { this.locked = false; this.notifyAll(); } }
Lock是JAVA5.0出現的,它的出現並不是為了替代synchronized,而是在synchronized不適用的時候使用。
那么synchronized有什么局限性呢?
- 無法中斷一個正在獲取鎖的線程
當一個線程想獲取已經被其他線程持有的鎖時,就會發生堵塞,假設已經持有鎖的線程一直不釋放鎖,那么線程就會一直等待下去。
- 無法指定獲得鎖的等待時間
比如,想要A線程執行某個操作,想在指定時間內A線程沒有獲取到鎖就返回。synchronized是做不到的。
相同點:
- 獨占鎖: 一次只允許一個線程訪問
- 可重入鎖: 一個線程可重復獲得自己已獲得鎖,不會發生死鎖。簡單來說,遞歸的時候不會發生死鎖
不同點:
- Lock不是java內置的,synchronized是JVM內置的,因此是內置特性。
- 釋放鎖的方式:
- Lock 必須要在finally中手動釋放鎖
- synchronized 會根據鎖區域代碼自動執行完畢,或者發生異常,JVM會自動釋放鎖
- 公平:
- Lock是可公平可不公平鎖
- synchronized是不公平鎖
ReentrantLock的使用:
基本使用
Lock lock = new ReentrantLock(); lock.lock(); try { //.... } finally { lock.unlock(); }
- lock: 調用后一直阻塞直到獲得鎖。
package com.amber; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestReentrantLock { Lock lock = new ReentrantLock(); public static void main(String[] args) { TestReentrantLock testReentrantLock = new TestReentrantLock(); new Thread(() -> { try { testReentrantLock.testConcurrency(Thread.currentThread()); } catch (InterruptedException e) { e.printStackTrace(); } }, "線程1").start(); new Thread(() -> { try { testReentrantLock.testConcurrency(Thread.currentThread()); } catch (InterruptedException e) { e.printStackTrace(); } }, "線程2").start(); } private void testConcurrency(Thread thread) throws InterruptedException { //獲取鎖成功返回true,如果獲取失敗,等待2S,規定時間內還是沒有獲得鎖,那么就返回false if (lock.tryLock( 2000, TimeUnit.MICROSECONDS)) { try { System.out.println(thread.getName() + " : " + "獲取鎖"); Thread.sleep(3000); } finally { System.out.println(thread.getName() + "釋放鎖"); lock.unlock(); //一定記得要釋放鎖 } } else { System.out.println(Thread.currentThread().getName() + " : " + "等待了,沒有獲取鎖"); } } }
- tryLock:拿到鎖返回true,否則false;帶有時間限制的tryLock(long time, TimeUnit timeUnit),拿不到鎖,就等待一段時間,超時返回false
- lockInterruptibly :調用后如果沒有獲取到鎖會一直阻塞,阻塞過程中會接受中斷信號。
lockInterruptibly有點難以理解,假設A線程想去獲取鎖,但是鎖被B線程持有,那么A就會發生堵塞。
A堵塞的時候,可以有以下兩種方法發生狀態改變:
- A獲取鎖資源
- A被其他線程中斷:
- 這里只得被其他線程中斷的意思是,C線程調用A線程的interrupt()。那么此時A線程就會被喚醒,處理中斷信號。
lockInterruptibly是被中斷,就由阻塞狀態被喚醒去處理中斷信號。
在JAVA並發編程實戰這本書中還提到了ReentrantLock的一個重要用法,那就是輪詢鎖。下面是書中的源代碼:
1 public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException { 2 long fixedDelay = 1; 3 long randMod = 2; 4 long stopTime = System.nanoTime() + unit.toNanos(timeout); 5 while (true) { 6 if (fromAcct.lock.tryLock()) { 7 try { 8 if (toAcct.lock.tryLock()) { //如果不能同事獲得兩個鎖,那么線程就會釋放已經獲得的鎖。這樣可以很有效的解決死鎖問題。 9 try { 10 if (fromAcct.getBalance().compareTo(amount) < 0) 11 throw new InsufficientFundsException(); 12 else{ 13 fromAcct.debit(amount); 14 toAcct.credit(amount); 15 returntrue; 16 } 17 } finally { 18 toAcct.lock.unlock(); 19 } 20 } 21 } finally { 22 fromAcct.lock.unlock(); 23 } 24 } 25 if (System.nanoTime() < stopTime) 26 returnfalse; 27 NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); 28 } 29 }
程序發生死鎖的時候,往往只能通過重新啟動程序解決。而有時候因為獲取鎖的時序不一致,很容易發生死鎖。根據上述代碼第6行和第8行,假設此時我們使用的synochronized內置鎖,A線程從cc賬號轉賬到dd賬號,B線程從dd賬號轉賬到cc賬號,就很容易發生死鎖。但是使用tryLock()卻可以避免鎖順序造成死鎖的問題,
如果線程A、B不能同時獲取cc和dd對象的鎖,那么就會放棄自己已經獲得的鎖。