1. 程式人生 > >談談Synchrnized和ReentrantLock區別

談談Synchrnized和ReentrantLock區別

 

今年參加校招筆試面試經常遇到的一個問題。

    總的來說,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的內建屬性,它能執行一些優化(鎖消除等)