併發編程的鎖機制:synchronized和lock

併發編程中,鎖是常常須要用到的,今天咱們一塊兒來看下Java中的鎖機制:synchronized和lock。java

1. 鎖的種類

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

1.1 可重入鎖

若是鎖具有可重入性,則稱做爲可重入鎖。synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上代表了鎖的分配機制:基於線程的分配,而不是基於方法調用的分配。舉好比說,當一個線程執行到method1 的synchronized方法時,而在method1中會調用另一個synchronized方法method2,此時該線程沒必要從新去申請鎖,而是能夠直接執行方法method2。bash

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的比較
相關文章
相關標籤/搜索