談談Synchrnized和ReentrantLock區別
阿新 • • 發佈:2018-11-27
今年參加校招筆試面試經常遇到的一個問題。
總的來說,ReentrantLock並不是一種替代內建加鎖的方法,而是當內建加鎖機制不適用時,作為一種可選擇的高階功能。與內建鎖不同的是,Lock提供了一種無條件的、可輪詢的、定時的以及可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯式的。
實現機制
- Synchronized通過java物件頭鎖標記和Monitor物件實現
- Reentrantlock通過CAS、ASQ(AbstractQueuedSynchronizer)和locksupport(用於阻塞和解除阻塞)實現
- Synchronized依賴jvm記憶體模型保證包含共享變數的多執行緒記憶體可見性
- Reentrantlock通過ASQ的volatile state保證包含共享變數的多執行緒記憶體可見
可中斷申請
如果使用synchronized申請一個內建鎖時鎖被其他執行緒持有, 那麼當前執行緒將被掛起, 等待鎖重新可用, 而且等待期間無法中斷. 而顯式鎖提供了可中斷申請: (B執行緒申請被A佔有鎖時,要等待,但可以用Thread.interrupt()中斷)
public class InterruptedLock extends Thread { private static Lock lock = new ReentrantLock(); @Override public void run() { try { // 可中斷申請, 在申請鎖的過程中如果當前執行緒被中斷, 將丟擲InterruptedException異常 lock.lockInterruptibly(); } catch (InterruptedException e) { System.out.println("interruption happened"); return; } // 如果執行到這裡, 說明已經申請到鎖, 且沒有發生異常 try { System.out.println("run is holding the lock"); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { try { lock.lock(); System.out.println("main is holding the lock."); Thread thread = new InterruptedLock(); thread.start(); // 1s後中斷thread執行緒, 該執行緒此時應該阻塞在lockInterruptibly方法上 Thread.sleep(1000); // 中斷thread執行緒將導致其丟擲InterruptedException異常. thread.interrupt(); Thread.sleep(1000); } finally { lock.unlock(); } } }
嘗試型申請
Lock.tryLock;
Lock.tryLock(long time, TimeUnit unit);
上述方法用於嘗試獲取鎖. 如果嘗試沒有成功, 則返回false, 否則返回true. 而內建鎖則不提供這種特性, 一旦開始申請內建鎖, 在申請成功之前, 執行緒無法中斷, 申請也無法取消.
Lock的嘗試型申請通常用於實現時間限定的task:
public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException { long fixedDelay = getFixedDelayComponentNanos(timeout, unit); long randMod = getRandomDelayModulusNanos(timeout, unit); // 截止時間 long stopTime = System.nanoTime() + unit.toNanos(timeout); while (true) { if (fromAcct.lock.tryLock()) { try { if (toAcct.lock.tryLock()) { try { if (fromAcct.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAcct.debit(amount); toAcct.credit(amount); return true; } } finally { // 成功申請到鎖時才需要釋放鎖 toAcct.lock.unlock(); } } } finally { // 成功申請到鎖時才需要釋放鎖 fromAcct.lock.unlock(); } } // 如果已經超過截止時間直接返回false, 說明轉賬沒有成功. 否則進行下次嘗試. if (System.nanoTime() < stopTime) return false; NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); } }
鎖的釋放(非塊結構的加鎖)
- 對於內建鎖, 只要程式碼執行到同步程式碼塊之外,就會自動釋放鎖,開發者無需擔心丟擲異常, 方法返回等情況發生時鎖會沒有被釋放的問題.
- 然而對於顯式鎖,必須呼叫unlock方法才能釋放鎖.此時需要開發者自己處理丟擲異常, 方法返回等情況.
- 通常會在finally程式碼塊中進行鎖的釋放,還需注意只有申請到鎖之後才需要釋放鎖, 釋放未持有的鎖可能會丟擲未檢查異常.
- 所以使用內建鎖更容易一些, 而顯式鎖則繁瑣很多.
- 但是顯式鎖釋放方式的繁瑣也帶來一個方便的地方:鎖的申請和釋放不必在同一個程式碼塊中.
喚醒和等待
- 執行緒可以wait在內建鎖上,也可以通過呼叫內建鎖的notify或notifyAll方法喚醒在其上等待的執行緒.
- 但是如果有多個執行緒在內建鎖上wait,我們無法精確喚醒其中某個特定的執行緒.顯式鎖也可以用於喚醒和等待.呼叫Lock.newCondition方法可以獲得Condition物件, 呼叫Condition.await方法將使得執行緒等待,呼叫Condition.singal或Condition.singalAll方法可以喚醒在該Condition物件上等待的執行緒. 由於同一個顯式鎖可以派生出多個Condition物件,因此我們可以實現精確喚醒.
- 通過ReentrantLock(boolean fair)建構函式建立ReentranLock鎖時可以 為其指定公平策略,預設情況下為不公平鎖.
- 多個執行緒申請公平鎖時,申請時間早的執行緒優先獲得鎖(佇列).然而不公平鎖則允許插隊,當某個執行緒申請鎖時如果鎖恰好可用,則該執行緒直接獲得鎖而不用排隊.
- 比如執行緒B申請某個不公平鎖時該鎖正在由執行緒A持有, 執行緒B將被掛起. 當執行緒A釋放鎖時, 執行緒B將從掛起狀態中恢復並打算再次申請(這個過程需要一定時間,導致沒非公平的好). 如果此時恰好執行緒C也來申請鎖,則不公平策略允許執行緒C立刻獲得鎖並開始執行.
- 內建鎖採用不公平策略,而顯式鎖則可以指定是否使用不公平策略.
- 非公平鎖能提高吞吐率
喚醒和等待
- 執行緒可以wait在內建鎖上,也可以通過呼叫內建鎖的notify或notifyAll方法喚醒在其上等待的執行緒.
- 但是如果有多個執行緒在內建鎖上wait,我們無法精確喚醒其中某個特定的執行緒.顯式鎖也可以用於喚醒和等待.呼叫Lock.newCondition方法可以獲得Condition物件, 呼叫Condition.await方法將使得執行緒等待,呼叫Condition.singal或Condition.singalAll方法可以喚醒在該Condition物件上等待的執行緒. 由於同一個顯式鎖可以派生出多個Condition物件,因此我們可以實現精確喚醒.
synchronized要被淘汰了?no
與顯式鎖相比,內建鎖仍然有很大優勢:
- 內建鎖為許多開發人員熟悉,並且簡潔緊湊,而且在許多現有的程式中都已經使用了內建鎖
- ReentrantLock危險性大,如果忘記unlock,後果不堪設想
- ReentrantLock的非塊結構特性仍然意味著,獲取鎖的操作不能與特定的棧幀關聯起來,而內建鎖可以
- synchronized是JVM的內建屬性,它能執行一些優化(鎖消除等)