Java多線程——鎖(Synchronized、Lock、ReentrantLock、ReadWriteLock、ReentrantReadWriteLock)

synchronized與Lock


  synchronized是java中的一個關鍵字,也就是說是Java語言內置的特性。那麼爲什麼會出現Lock呢?

  在上面一篇文章中,我們瞭解到如果一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,並執行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這裏獲取鎖的線程釋放鎖只會有兩種情況:

  1)獲取鎖的線程執行完了該代碼塊,然後線程釋放對鎖的佔有;

  2)線程執行發生異常,此時JVM會讓線程自動釋放鎖。

  那麼如果這個獲取鎖的線程由於要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能乾巴巴地等待,試想一下,這多麼影響程序執行效率。

  因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。

  再舉個例子:當有多個線程讀寫文件時,讀操作和寫操作會發生衝突現象,寫操作和寫操作會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。

  但是採用synchronized關鍵字來實現同步的話,就會導致一個問題:

  如果多個線程都只是進行讀操作,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。

  因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發生衝突,通過Lock就可以辦到。

  另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的。

  總結一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:

  1)Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問;

  2)Lock和synchronized有一點非常大的不同,採用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之後,系統會自動讓線程釋放對鎖的佔用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。

Lock


通過源碼可以知道,Lock是一個接口

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

1. 方法

  1. lock()
    用來獲取鎖。如果鎖已被其他線程獲取,則等待。

    Lock lock = ...;
    if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){
    
     }finally{
         lock.unlock();   //釋放鎖
     } 
    }else {
    //如果不能獲取鎖,則直接做其他事情
    }
  2. tryLock()
    用來獲取鎖。如果鎖已被其他線程獲取,則返回false,否則返回true。不會進行等待。

    Lock lock = ...;
    if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){
    
     }finally{
         lock.unlock();   //釋放鎖
     } 
    }else {
    //如果不能獲取鎖,則直接做其他事情
    }
  3. tryLock(long time, TimeUnit unit)
    與tryLock()方法類似,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

  4. lockInterruptibly()
    當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。

    public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
    }

ReentrantLock

Lock接口實現類
是一個獨佔鎖,與sychronized類似


ReadWriteLock

也是一個接口

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

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

ReentrantReadWriteLock

是ReadWriteLock的實現類

ReentrantReadWriteLock裏面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。

ReentrantReadWriteLock裏面的鎖主體就是一個Sync,也就是FairSync或者NonfairSync,所以說實際上只有一個鎖,只是在獲取讀取鎖和寫入鎖的方式上不一樣。

ReentrantReadWriteLock裏面有兩個類:ReadLock/WriteLock,這兩個類都是Lock的實現。

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

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

特點:

  • 公平性
    非公平鎖(默認) 這個和獨佔鎖的非公平性一樣,由於讀線程之間沒有鎖競爭,所以讀操作沒有公平性和非公平性,寫操作時,由於寫操作可能立即獲取到鎖,所以會推遲一個或多個讀操作或者寫操作。因此非公平鎖的吞吐量要高於公平鎖。
    公平鎖 利用AQS的CLH隊列,釋放當前保持的鎖(讀鎖或者寫鎖)時,優先爲等待時間最長的那個寫線程分配寫入鎖,當前前提是寫線程的等待時間要比所有讀線程的等待時間要長。同樣一個線程持有寫入鎖或者有一個寫線程已經在等待了,那麼試圖獲取公平鎖的(非重入)所有線程(包括讀寫線程)都將被阻塞,直到最先的寫線程釋放鎖。如果讀線程的等待時間比寫線程的等待時間還有長,那麼一旦上一個寫線程釋放鎖,這一組讀線程將獲取鎖。
  • 重入性 讀寫鎖允許讀線程和寫線程按照請求鎖的順序重新獲取讀取鎖或者寫入鎖。當然了只有寫線程釋放了鎖,讀線程才能獲取重入鎖。
    寫線程獲取寫入鎖後可以再次獲取讀取鎖,但是讀線程獲取讀取鎖後卻不能獲取寫入鎖。
    另外讀寫鎖最多支持65535個遞歸寫入鎖和65535個遞歸讀取鎖。
  • 鎖降級
    寫線程獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。
  • 鎖升級
    讀取鎖是不能直接升級爲寫入鎖的。因爲獲取一個寫入鎖需要釋放所有讀取鎖,所以如果有兩個讀取鎖視圖獲取寫入鎖而都不釋放讀取鎖時就會發生死鎖。
  • 鎖獲取中斷 讀
    取鎖和寫入鎖都支持獲取鎖期間被中斷。這個和獨佔鎖一致。
  • 條件變量
    寫入鎖提供了條件變量(Condition)的支持,這個和獨佔鎖一致,但是讀取鎖卻不允許獲取條件變量,將得到一個UnsupportedOperationException異常。
  • 重入數
    讀取鎖和寫入鎖的數量最大分別只能是65535(包括重入數)。
public class ReentrantReadWriteLockUse {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final ReentrantReadWriteLockUse test = new ReentrantReadWriteLockUse();

        new Thread() {
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();

        new Thread() {
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();

    }

    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();

            while (System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName() + "正在進行讀操作");
            }
            System.out.println(thread.getName() + "讀操作完畢");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

運行結果

Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1讀操作完畢
Thread-0讀操作完畢

小結

ReentrantReadWriteLock相比ReentrantLock的最大區別是:ReentrantReadWriteLock的讀鎖是共享鎖,任何線程都可以獲取,而寫鎖是獨佔鎖。ReentrantLock不論讀寫,是獨佔鎖。


總結——Lock和synchronized的選擇

Lock和synchronized有以下幾點不同:
1. Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;
2. synchronized在發生異常時,會自動釋放線程佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
3. Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
4. 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
5. Lock可以提高多個線程進行讀操作的效率。
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。

鎖的概念相關介紹

  1. 可重入鎖
    如果鎖具備可重入性,則稱作爲可重入鎖。像synchronized和ReentrantLock都是可重入鎖
  2. 可中斷鎖
    可中斷鎖:顧名思義,就是可以interrupt()中斷的鎖。
    在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
    如果某一線程A正在執行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由於等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
    在前面演示lockInterruptibly()的用法時已經體現了Lock的可中斷性。
  3. 公平鎖
    公平鎖即儘量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
    非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖。
    在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。
    而對於ReentrantLock和ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設置爲公平鎖。
  4. 讀寫鎖
    讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
    正因爲有了讀寫鎖,才使得多個線程之間的讀操作不會發生衝突,提高了程序的性能。
    ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。
    可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

參考資料:
http://www.cnblogs.com/dolphin0520/p/3923167.html
http://my.oschina.net/adan1/blog/158107

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