lock與synchronized區別詳解

其實日常開發中,大多時間沒有使用到lock以及synchronized的場景,導致有些遺忘了,這裏再梳理一下。

1.各自優缺點

synchronized其實是java語言中的一個關鍵字,是內置的特性,這就導致它在使用上有一定的簡潔性,如果使用lock,它作爲接口的實現類,具備更強大的功能,但是需要自己確保鎖的正確釋放,各自優缺點如下:

  • 當synchronized塊結束時,會自動釋放鎖,lock一般需要在finally中自己釋放。
  • 當synchronized塊執行異常時,也會自動釋放鎖,lock一般需要在finally中自己釋放。
  • synchronized使用起來簡單,不需要進行鎖的額外處理,當成關鍵字使用,而lock一般需要創建對象,手動鎖住,手動釋放。
  • 當synchronized塊執行阻塞時,例如等待IO或者sleep方法時,無法中斷,並釋放鎖,lock可以在線程休眠時,執行中斷,提高性能。
  • 當synchronized塊執行時,只能使用非公平鎖,無法實現公平鎖,而lock可以通過new ReentrantLock(true)設置爲公平鎖,從而在某些場景下提高效率。
  • 使用synchronized 無法判斷當前線程是否成功獲取到鎖,lock可以通過trylock進行判斷。
  • 另外看資料,synchronized併發是通過阻塞其它線程實現的,線程再進行恢復時,是cpu內核級別的切換,性能較低,後面經過jdk的優化才逐步與lock效率對齊,但是併發場景非常高的情況下,還是lock性能較好。

2.java.util.concurrent.locks包常用類

2.1 Lock

其實是一個接口,定義了幾個方法:

public interface Lock {
    void 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塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:

    Lock lock = new ReentrantLock();
        lock.lock();
        try {
            //處理任務
        } catch (Exception ex) {
    
        } finally {
            lock.unlock(); //釋放鎖
        }
    
  • tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。

  •  tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。所以,一般情況下通過tryLock來獲取鎖時是這樣使用的:

    Lock lock = new ReentrantLock();
        if (lock.tryLock()) {
            try {
                //處理任務
            } catch (Exception ex) {
    
            } finally {
                lock.unlock(); //釋放鎖
            }
        } else {
            //如果不能獲取鎖,則直接做其他事情
        }
    
  • lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。一般的使用形式如下:

        Lock lock = new ReentrantLock();
        lock.lockInterruptibly();
        try {
            //.....
        }
        finally {
            lock.unlock();
        }
    

  注意,當一個線程獲取了鎖之後,是不會被interrupt()方法中斷的。因爲本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。
  因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。
  而用synchronized修飾的話,當一個線程處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。

2.2 ReentrantLock

  ReentrantLock,意思是“可重入鎖”,ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法。
詳見:java.util.concurrent.locks.ReentrantLock ,不再列舉了。

2.3 ReadWriteLock

接口,只定義了兩個方法:

    Lock readLock();
    Lock writeLock();

一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。

2.4 ReentrantReadWriteLock

實現了ReadWriteLock接口。

下面嘗試寫個例子,表示ReadWriteLock和使用synchronized的區別。

使用synchronized時,讀取文件:

public class SynchronizedReadFile {
    public static void main(String[] args) {
        final SynchronizedReadFile synchronizedReadFile = new SynchronizedReadFile();

        new Thread() {
            public void run() {
                synchronizedReadFile.read(Thread.currentThread());
            }
        }.start();

        new Thread() {
            public void run() {
                synchronizedReadFile.read(Thread.currentThread());
            }
        }.start();
    }

    public synchronized void read(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在進行讀操作");
        }
        System.out.println(thread.getName()+"讀操作完畢");
    }

}

結果是 thread0讀取完成,纔開始讀取thread1,其實兩個讀取操作並不要求互斥,可以同時讀取提高效率。
下面是使用reenterantReadWriteLock:

public class ReentrantReadWriteFile {

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final ReentrantReadWriteFile reentrantReadWriteFile = new ReentrantReadWriteFile();

        new Thread() {
            public void run() {
                reentrantReadWriteFile.read(Thread.currentThread());
            }
        }.start();

        new Thread() {
            public void run() {
                reentrantReadWriteFile.read(Thread.currentThread());
            }
        }.start();

    }

    public void read(Thread thread) {
        readWriteLock.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName() + "正在進行讀操作");
            }
            System.out.println(thread.getName() + "讀操作完畢");
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

可以看到thread-0 和thread-1是交替同時讀取的,極大的提高了效率。

不過要注意的是:
1.如果有一個線程已經佔用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。

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

3.下面列了一下鎖的相關的介紹:

1.可重入鎖

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

class MyClass {
    public synchronized void method1() {
        method2();
    }
     
    public synchronized void method2() {
         
    }
}

上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執行到了method1,此時線程A獲取了這個對象的鎖,而由於method2也是synchronized方法,假如synchronized不具備可重入性,此時線程A需要重新申請鎖。但是這就會造成一個問題,因爲線程A已經持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。

而由於synchronized和Lock都具備可重入性,所以不會發生上述現象。

2.可中斷鎖

可中斷鎖:顧名思義,就是可以相應中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。

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

lockInterruptibly()的用法時已經體現了Lock的可中斷性。

3.公平鎖

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

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

在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。

而對於ReentrantLock和ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設置爲公平鎖。

4.讀寫鎖

讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。

正因爲有了讀寫鎖,才使得多個線程之間的讀操作不會發生衝突。

ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。

可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章