併發程式設計的鎖機制:synchronized和lock

aoho發表於2017-12-27

併發程式設計中,鎖是經常需要用到的,今天我們一起來看下Java中的鎖機制:synchronized和lock。

1. 鎖的種類

鎖的種類挺多,包括:自旋鎖、自旋鎖的其他種類、阻塞鎖、可重入鎖、讀寫鎖、互斥鎖、悲觀鎖、樂觀鎖、公平鎖、可重入鎖等等,其餘就不列出了。我們這邊重點看如下幾種:可重入鎖、讀寫鎖、可中斷鎖、公平鎖。

1.1 可重入鎖

如果鎖具備可重入性,則稱作為可重入鎖。synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明瞭鎖的分配機制:基於執行緒的分配,而不是基於方法呼叫的分配。舉比如說,當一個執行緒執行到method1 的synchronized方法時,而在method1中會呼叫另外一個synchronized方法method2,此時該執行緒不必重新去申請鎖,而是可以直接執行方法method2。

1.2 讀寫鎖

讀寫鎖將對一個資源的訪問分成了2個鎖,如檔案,一個讀鎖和一個寫鎖。正因為有了讀寫鎖,才使得多個執行緒之間的讀操作不會發生衝突。ReadWriteLock就是讀寫鎖,它是一個介面,ReentrantReadWriteLock實現了這個介面。可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

1.3 可中斷鎖

可中斷鎖,即可以中斷的鎖。在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。 如果某一執行緒A正在執行鎖中的程式碼,另一執行緒B正在等待獲取該鎖,可能由於等待時間過長,執行緒B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。

Lock介面中的lockInterruptibly()方法就體現了Lock的可中斷性。

1.4 公平鎖

公平鎖即儘量以請求鎖的順序來獲取鎖。同時有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該鎖,這種就是公平鎖。

非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的,這樣就可能導致某個或者一些執行緒永遠獲取不到鎖。

synchronized是非公平鎖,它無法保證等待的執行緒獲取鎖的順序。對於ReentrantLockReentrantReadWriteLock,預設情況下是非公平鎖,但是可以設定為公平鎖。

2. synchronized和lock的用法

2.1 synchronized

synchronized是Java的關鍵字,當它用來修飾一個方法或者一個程式碼塊的時候,能夠保證在同一時刻最多隻有一個執行緒執行該段程式碼。簡單總結如下四種用法。

2.1.1 程式碼塊

對某一程式碼塊使用,synchronized後跟括號,括號裡是變數,一次只有一個執行緒進入該程式碼塊。

public int synMethod(int m){
    synchronized(m) {
     //...
    }
 }
複製程式碼

2.1.2 方法宣告時

方法宣告時使用,放在範圍操作符之後,返回型別宣告之前。即一次只能有一個執行緒進入該方法,其他執行緒要想在此時呼叫該方法,只能排隊等候。

public synchronized void synMethod() {
   //...
}
複製程式碼

2.1.3 synchronized後面括號裡是物件

synchronized後面括號裡是一物件,此時執行緒獲得的是物件鎖。

public void test() {
  synchronized (this) {
      //...
  }
}
複製程式碼

2.1.4 synchronized後面括號裡是類

synchronized後面括號裡是類,如果執行緒進入,則執行緒在該類中所有操作不能進行,包括靜態變數和靜態方法,對於含有靜態方法和靜態變數的程式碼塊的同步,通常使用這種方式。

2.2 Lock

Lock介面主要相關的類和介面如下。

Lock
Lock

ReadWriteLock是讀寫鎖介面,其實現類為ReetrantReadWriteLock。ReetrantLock實現了Lock介面。

2.2.1 Lock

Lock中有如下方法:

public interface Lock {
	void lockInterruptibly() throws InterruptedException;  
	boolean tryLock();  
	boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  
	void unlock();  
	Condition newCondition();
}
複製程式碼
  • lock:用來獲取鎖,如果鎖被其他執行緒獲取,處於等待狀態。如果採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。

  • lockInterruptibly:通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則這個執行緒能夠響應中斷,即中斷執行緒的等待狀態。

  • tryLock:tryLock方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。

  • tryLock(long,TimeUnit):與tryLock類似,只不過是有等待時間,在等待時間內獲取到鎖返回true,超時返回false。

  • unlock:釋放鎖,一定要在finally塊中釋放

2.2.2 ReetrantLock

實現了Lock介面,可重入鎖,內部定義了公平鎖與非公平鎖。預設為非公平鎖:

public ReentrantLock() {  
  sync = new NonfairSync();  
} 
複製程式碼

可以手動設定為公平鎖:

public ReentrantLock(boolean fair) {  
  sync = fair ? new FairSync() : new NonfairSync();  
}  
複製程式碼

2.2.3 ReadWriteLock

public interface ReadWriteLock {  
    Lock readLock();       //獲取讀鎖  
    Lock writeLock();      //獲取寫鎖  
}  
複製程式碼

一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將檔案的讀寫操作分開,分成2個鎖來分配給執行緒,從而使得多個執行緒可以同時進行讀操作。ReentrantReadWirteLock實現了ReadWirteLock介面,並未實現Lock介面。 不過要注意的是:

如果有一個執行緒已經佔用了讀鎖,則此時其他執行緒如果要申請寫鎖,則申請寫鎖的執行緒會一直等待釋放讀鎖。

如果有一個執行緒已經佔用了寫鎖,則此時其他執行緒如果申請寫鎖或者讀鎖,則申請的執行緒會一直等待釋放寫鎖。

2.2.4 ReetrantReadWriteLock

ReetrantReadWriteLock同樣支援公平性選擇,支援重進入,鎖降級。

public class RWLock {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    static Lock r = rwLock.readLock();
    static Lock w = rwLock.writeLock();
    //讀
    public static final Object get(String key){
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
    //寫
    public static final Object put(String key, Object value){
        w.lock();
        try {
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }
}
複製程式碼

只需在讀操作時獲取讀鎖,寫操作時獲取寫鎖。當寫鎖被獲取時,後續的讀寫操作都會被阻塞,寫鎖釋放後,所有操作繼續執行。

3. 兩種鎖的比較

3.1 synchronized和lock的區別

  • Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內建的語言實現;
  • synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
  • Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;
  • 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
  • Lock可以提高多個執行緒進行讀操作的效率。(可以通過readwritelock實現讀寫分離)
  • 效能上來說,在資源競爭不激烈的情形下,Lock效能稍微比synchronized差點(編譯程式通常會盡可能的進行優化synchronized)。但是當同步非常激烈的時候,synchronized的效能一下子能下降好幾十倍。而ReentrantLock確還能維持常態。

3.2 效能比較

下面對synchronized與Lock進行效能測試,分別開啟10個執行緒,每個執行緒計數到1000000,統計兩種鎖同步所花費的時間。網上也能找到這樣的例子。

public class TestAtomicIntegerLock {

    private static int synValue;

    public static void main(String[] args) {
        int threadNum = 10;
        int maxValue = 1000000;
        testSync(threadNum, maxValue);
        testLocck(threadNum, maxValue);
    }
	//test synchronized
    public static void testSync(int threadNum, int maxValue) {
        Thread[] t = new Thread[threadNum];
        Long begin = System.nanoTime();
        for (int i = 0; i < threadNum; i++) {
            Lock locks = new ReentrantLock();
            synValue = 0;
            t[i] = new Thread(() -> {

                for (int j = 0; j < maxValue; j++) {
                    locks.lock();
                    try {
                        synValue++;
                    } finally {
                        locks.unlock();
                    }
                }

            });
        }
        for (int i = 0; i < threadNum; i++) {
            t[i].start();
        }
        //main執行緒等待前面開啟的所有執行緒結束
        for (int i = 0; i < threadNum; i++) {
            try {
                t[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("使用lock所花費的時間為:" + (System.nanoTime() - begin));
    }
	// test Lock
    public static void testLocck(int threadNum, int maxValue) {
        int[] lock = new int[0];
        Long begin = System.nanoTime();
        Thread[] t = new Thread[threadNum];
        for (int i = 0; i < threadNum; i++) {
            synValue = 0;
            t[i] = new Thread(() -> {
                for (int j = 0; j < maxValue; j++) {
                    synchronized(lock) {
                        ++synValue;
                    }
                }
            });
        }
        for (int i = 0; i < threadNum; i++) {
            t[i].start();
        }
        //main執行緒等待前面開啟的所有執行緒結束
        for (int i = 0; i < threadNum; i++) {
            try {
                t[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("使用synchronized所花費的時間為:" + (System.nanoTime() - begin));
    }

}
複製程式碼

測試結果的差異還是比較明顯的,Lock的效能明顯高於synchronized。本次測試基於JDK1.8。

使用lock所花費的時間為:436667997
使用synchronized所花費的時間為:616882878
複製程式碼

JDK1.5中,synchronized是效能低效的。因為這是一個重量級操作,它對效能最大的影響是阻塞的是實現,掛起執行緒和恢復執行緒的操作都需要轉入核心態中完成,這些操作給系統的併發性帶來了很大的壓力。相比之下使用Java提供的Lock物件,效能更高一些。多執行緒環境下,synchronized的吞吐量下降的非常嚴重,而ReentrankLock則能基本保持在同一個比較穩定的水平上。

到了JDK1.6,發生了變化,對synchronize加入了很多優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在JDK1.6上synchronize的效能並不比Lock差。官方也表示,他們也更支援synchronize,在未來的版本中還有優化餘地,所以還是提倡在synchronized能實現需求的情況下,優先考慮使用synchronized來進行同步。

4. 總結

本文主要對併發程式設計中的鎖機制synchronized和lock,進行詳解。synchronized是基於JVM實現的,內建鎖,Java中的每一個物件都可以作為鎖。對於同步方法,鎖是當前例項物件。對於靜態同步方法,鎖是當前物件的Class物件。對於同步方法塊,鎖是Synchonized括號裡配置的物件。Lock是基於在語言層面實現的鎖,Lock鎖可以被中斷,支援定時鎖。Lock可以提高多個執行緒進行讀操作的效率。通過對比得知,Lock的效率是明顯高於synchronized關鍵字的,一般對於資料結構設計或者框架的設計都傾向於使用Lock而非Synchronized。

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考

  1. Lock和synchronized比較詳解
  2. Java中Lock和synchronized的比較和應用
  3. Java併發程式設計(六)--Lock與Synchronized的比較

相關文章