java 鎖 Lock接口詳解

一:java.util.concurrent.locks包下常用的類與接口(lock是jdk 1.5後新增的)

  (1)Lock和ReadWriteLock是兩大鎖的根接口,Lock代表實現類是ReentrantLock(可重入鎖),ReadWriteLock(讀寫鎖)的代表實現類是ReentrantReadWriteLock。

    Lock 接口支持那些語義不同(重入、公平等)的鎖規則,可以在非阻塞式結構的上下文(包括 hand-over-hand 和鎖重排算法)中使用這些規則。主要的實現是 ReentrantLock。
ReadWriteLock 接口以類似方式定義了一些讀取者可以共享而寫入者獨佔的鎖。此包只提供了一個實現,即 ReentrantReadWriteLock,因爲它適用於大部分的標準用法上下文。但程序員可以創建自己的、適用於非標準要求的實現。
  (2)Condition 接口描述了可能會與鎖有關聯的條件變量。這些變量在用法上與使用 Object.wait 訪問的隱式監視器類似,但提供了更強大的功能。需要特別指出的是,單個 Lock 可能與多個 Condition 對象關聯。爲了避免兼容性問題,Condition 方法的名稱與對應的 Object 版本中的不同。


二:synchronized的缺陷

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

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

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

   synchronized 的侷限性 與 Lock 的優點 

  如果一個代碼塊被synchronized關鍵字修飾,當一個線程獲取了對應的鎖,並執行該代碼塊時,其他線程便只能一直等待直至佔有鎖的線程釋放鎖。事實上,佔有鎖的線程釋放鎖一般會是以下三種情況之一:

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

  2:佔有鎖線程執行發生異常,此時JVM會讓線程自動釋放鎖;

  3:佔有鎖線程進入 WAITING 狀態從而釋放鎖,例如在該線程中調用wait()方法等。

  試考慮以下三種情況: 

Case 1 :

  在使用synchronized關鍵字的情形下,假如佔有鎖的線程由於要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,那麼其他線程就只能一直等待,別無他法。這會極大影響程序執行效率。因此,就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間 (解決方案:tryLock(long time, TimeUnit unit)) 或者 能夠響應中斷 (解決方案:lockInterruptibly())),這種情況可以通過 Lock 解決。

Case 2 :

  我們知道,當多個線程讀寫文件時,讀操作和寫操作會發生衝突現象,寫操作和寫操作也會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。但是如果採用synchronized關鍵字實現同步的話,就會導致一個問題,即當多個線程都只是進行讀操作時,也只有一個線程在可以進行讀操作,其他線程只能等待鎖的釋放而無法進行讀操作。因此,需要一種機制來使得當多個線程都只是進行讀操作時,線程之間不會發生衝突。同樣地,Lock也可以解決這種情況 (解決方案:ReentrantReadWriteLock) 。

Case 3 :

  我們可以通過Lock得知線程有沒有成功獲取到鎖 (解決方案:ReentrantLock) ,但這個是synchronized無法辦到的。

上面提到的三種情形,我們都可以通過Lock來解決,但 synchronized 關鍵字卻無能爲力。事實上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 實現提供了比 synchronized 關鍵字 更廣泛的鎖操作,它能以更優雅的方式處理線程同步問題。也就是說,Lock提供了比synchronized更多的功能。


三:Lock接口實現類的使用

Lock接口有6個方法:

// 獲取鎖  
void lock()   

// 如果當前線程未被中斷,則獲取鎖,可以響應中斷  
void lockInterruptibly()   

// 返回綁定到此 Lock 實例的新 Condition 實例  
Condition newCondition()   

// 僅在調用時鎖爲空閒狀態才獲取該鎖,可以響應中斷  
boolean tryLock()   

// 如果鎖在給定的等待時間內空閒,並且當前線程未被中斷,則獲取鎖  
boolean tryLock(long time, TimeUnit unit)   

// 釋放鎖  
void unlock()

  下面來逐個分析Lock接口中每個方法。lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用來獲取鎖的。unLock()方法是用來釋放鎖的。newCondition() 返回 綁定到此 Lock 的新的 Condition 實例 ,用於線程間的協作,詳細內容請查找關鍵詞:線程間通信與協作。

1). lock()

  在Lock中聲明瞭四個方法來獲取鎖,那麼這四個方法有何區別呢?首先,lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。在前面已經講到,如果採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此,一般來說,使用Lock必須在try…catch…塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){

}finally{
    lock.unlock();   //釋放鎖
}

2). tryLock() & tryLock(long time, TimeUnit unit)

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

  tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false,同時可以響應中斷。如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

  一般情況下,通過tryLock來獲取鎖時是這樣使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){

     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖,則直接做其他事情
}

3). lockInterruptibly() 

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

  由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出 InterruptedException,但推薦使用後者,原因稍後闡述。因此,lockInterruptibly()一般的使用形式如下:

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

  注意,當一個線程獲取了鎖之後,是不會被interrupt()方法中斷的。因爲interrupt()方法只能中斷阻塞過程中的線程而不能中斷正在運行過程中的線程。因此,當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,那麼只有進行等待的情況下,纔可以響應中斷的。與 synchronized 相比,當一個線程處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。


Lock的實現類 ReentrantLock

  ReentrantLock,即 可重入鎖。ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法。下面通過一些實例學習如何使用 ReentrantLock。

  構造方法(不帶參數 和帶參數  true: 公平鎖; false: 非公平鎖):

/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

 

import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;
public class LockThread {
    Lock lock = new ReentrantLock(); 
    public void lock(String name) {  
        // 獲取鎖  
        lock.lock();  
        try {  
            System.out.println(name + " get the lock");  
            // 訪問此鎖保護的資源  
        } finally {  
            // 釋放鎖  
            lock.unlock();  
            System.out.println(name + " release the lock");  
        }  
    }  

    public static void main(String[] args) {
        LockThread lt = new LockThread();  
        new Thread(() -> lt.lock("A")).start();  
        new Thread(() -> lt.lock("B")).start();  
    }
}

  從執行結果可以看出,A線程和B線程同時對資源加鎖,A線程獲取鎖之後,B線程只好等待,直到A線程釋放鎖B線程才獲得鎖。

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

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

    2)synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中

    3)在資源競爭不是很激烈的情況下,Synchronized的性能要優於ReetrantLock,但是在資源競爭很激烈的情況下,Synchronized的性能會下降幾十倍,但是ReetrantLock的性能能維持常態。


 

ReadWriteLock鎖

ReadWriteLock 接口只有兩個方法:

//返回用於讀取操作的鎖  
Lock readLock()   
//返回用於寫入操作的鎖  
Lock writeLock() 

ReadWriteLock 維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。只要沒有 writer,讀取鎖可以由多個 reader 線程同時保持,而寫入鎖是獨佔的。

【例子】三個線程同時對一個共享數據進行讀寫

import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Queue {
    //共享數據,只能有一個線程能寫該數據,但可以有多個線程同時讀該數據。
    private Object data = null;

    ReadWriteLock lock = new ReentrantReadWriteLock();

    // 讀數據
    public void get() {
        // 加讀鎖
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " be ready to read data!");
            Thread.sleep((long) (Math.random() * 1000));
            System.out.println(Thread.currentThread().getName() + " have read data :" + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 釋放讀鎖
            lock.readLock().unlock();
        }
    }

    // 寫數據
    public void put(Object data) {
        // 加寫鎖
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " be ready to write data!");
            Thread.sleep((long) (Math.random() * 1000));
            this.data = data;
            System.out.println(Thread.currentThread().getName() + " have write data: " + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 釋放寫鎖
            lock.writeLock().unlock();
        }

    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        final Queue queue = new Queue();
        //一共啓動6個線程,3個讀線程,3個寫線程
        for (int i = 0; i < 3; i++) {
            //啓動1個讀線程
            new Thread() {
                public void run() {
                    while (true) {
                        queue.get();
                    }
                }

            }.start();
            //啓動1個寫線程
            new Thread() {
                public void run() {
                    while (true) {
                        queue.put(new Random().nextInt(10000));
                    }
                }
            }.start();
        }
    }
}

執行結果


四:鎖的相關概念介紹

  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不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。在前面演示tryLock(long time, TimeUnit unit)和lockInterruptibly()的用法時已經體現了Lock的可中斷性。

   3、公平鎖

  公平鎖即 儘量 以請求鎖的順序來獲取鎖。比如,同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。而非公平鎖則無法保證鎖的獲取是按照請求鎖的順序進行的,這樣就可能導致某個或者一些線程永遠獲取不到鎖。

  在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。而對於ReentrantLock 和 ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設置爲公平鎖

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