多線程—Lock、Condition、ReentrantLock、ReentrantReadWriteLock

Lock接口

public interface Lock {
//下面4個方法都是獲得鎖
  void lock();    
  void lockInterruptibly() throws InterruptedException; // 如果當前線程未被中斷,則獲取鎖,可以響應中斷  
  boolean tryLock();    //如果獲取到鎖返回true,否則false
  boolean tryLock(long var1, TimeUnit var3) throws InterruptedException; //如果獲取不到鎖,就等待一段時間,超時返回false。
//解除鎖,在finally裏調用
  void unlock();
//返回Condition實例
  Condition newCondition();
}

對於Lock接口方法的實現,大多都是調用AQS的方法來實現。實現Lock接口的類含有繼承了AQS一個內部類(例如ReentrantLock的Sync內部類),從而調用內部類的繼承自AQS的方法或者重寫的方法來實現Lock接口的方法。

Lock接口最後還有一個返回Condition實例的方法。Condition是和Lock配合使用的。

在一個AQS同步器中,可以定義多個Condition,每一個Condition是一條FIFO隊列。只需要多次lock.newCondition(),每次都會返回一個新的ConditionObject對象。在ConditionObject中,通過一個條件隊列來維護等待的線程,這個隊列跟AQS的隊列不是同一條隊列,一個同步器中可以有多個條件隊列。

Condition接口

public interface Condition {

    // 當前線程進入等待狀態直到被通知(signal)或被中斷
    void await() throws InterruptedException;
    // 不響應中斷等待,直到被通知(signal)
    void awaitUninterruptibly();
    // 等待指定時長直到被通知或中斷或超時。
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    // 等待指定時長直到被通知或中斷或超時。
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    // 當前線程進入等待狀態直到被通知、中斷或者到某個時間。如果沒有到指定事件就被通知,方法返回true,否則false。 
    boolean awaitUntil(Date deadline) throws InterruptedException;
    // 喚醒一個等待在Condition上的線程,該線程從等待方法返回前必須獲得與Condition相關聯的鎖  
    void signal();
    // 喚醒所有等待在Condition上的線程,該線程從等待方法返回前必須獲得與Condition相關聯的鎖  
    void signalAll();
}

ConditionObject

ConditionObject是AQS中的內部類,提供了條件鎖的同步實現,實現了Condition接口,並且實現了其中的await(),signal(),signalALL()等方法。因爲Condition的操作需要獲取相關聯的鎖,所以作爲同步器的內部類也較爲合理。每個Condition對象都包含着一個FIFO等待隊列,該隊列是Condition對象實現等待/通知功能的關鍵。

當線程獲取到鎖之後,Condition對象調用await相關的方法:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 添加當前線程到條件隊列
    Node node = addConditionWaiter();
    // 釋放已經獲取的鎖資源,並返回釋放前的同步狀態
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 如果當前節點不在同步隊列中,線程進入阻塞狀態,等待被喚醒
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

可以看到因爲ConditionObject是AQS內部類,可以獲取到外部類的數據,調用await將線程放進條件隊列,然後同樣調用LockSupport類的park方法進行阻塞。

Condition對象調用signal或者signalAll方法時:

// 將條件隊列中第一個有效的元素移除並且添加到同步隊列中
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    // 將條件隊列中等待最久的那個有效元素添加到同步隊列中
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

// 將條件隊列中的節點轉換到同步隊列中
final boolean transferForSignal(Node node) {
    // 如果節點的等待狀態不能被修改,說明當前線程已經被取消等待
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 加入到同步隊列中,並且嘗試將前驅節點設置爲可喚醒狀態
    Node p = enq(node); 
    int ws = p.waitStatus;
    // 如果前驅節點不需要喚醒,或者設置狀態爲‘喚醒’失敗,則喚醒線程時期重新爭奪同步狀態
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

singal方法將節點放回同步隊列尾端,調用enq方法,該方法在AQS的時候分析過,該方法使用了死循環, 即以自旋方式將節點插入隊列,如果失敗則不停的嘗試, 直到成功爲止,運用到了樂觀鎖的原理,該方法也負責在隊列爲空時, 初始化隊列。

爲什麼要有Condition呢?

在synchronized中,我們可以調用object.await和object.notify,讓線程等待和喚醒,但方法僅在synchronized中可以使用。通過Condition能夠更加精細的控制多線程的休眠與喚醒,synchronized相比ReentrantLock、ReadWriteLock等實現了Lock接口和內含繼承AQS類的子類的鎖來說,synchronized顯得笨重,不夠靈活,適合小段代碼使用。

condition.signal和object.notify區別

  • obj.notify();   隨機喚醒一個處於等待狀態的線程,可能有多個線程處於等待狀態,繼續執行wait後面的代碼。與synchronized同步關鍵字配合
  • condition.signal(); 喚醒在同條件隊列的線程,與Lock配合

使用要求:

  • signal()、await()、signalAll()方法使用之前必須要先進行lock()獲取鎖,類似使用Object的notify()、wait()、notifyAll()之前必須要對Object對象進行synchronized操作,且兩類方法不能混合使用,否則都會拋IIlegalMonitorStateException。

Synchronized和Lock比較

  • Synchronized是關鍵字,內置語言實現,Lock是接口。
  • Synchronized在線程發生異常時會自動釋放鎖,因此不會發生異常死鎖。Lock異常時不會自動釋放鎖,所以需要在finally中實現釋放鎖。
  • Lock是可以中斷鎖,Synchronized是非中斷鎖,必須等待線程執行完成釋放鎖。
  • Lock可以使用讀鎖提高多線程讀效率。

關係圖:

重入鎖

可重入就是說某個線程已經獲得某個鎖,可以再次獲取鎖而不會出現死鎖。

爲什麼需要再次獲取鎖呢?

一個線程在執行一個帶鎖的方法,該方法中又調用了另一個需要相同鎖的方法,則該線程可以直接執行調用的方法,而無需重新獲得鎖,從而避免了死鎖。

實現原理

通過爲每個鎖關聯一個請求計數器和一個獲得該鎖的線程。該計數器不是AQS的state,當計數器爲0時,認爲鎖是未被佔用的。線程請求一個未被佔用的鎖時,JVM將記錄該線程並將請求計數器設置爲1,此時該線程就獲得了鎖,當該線程再次請求這個鎖,計數器將遞增,當線程退出同步方法或者同步代碼塊時,計數器將遞減,當計數器爲0時,線程就釋放了該對象,其他線程才能獲取該鎖。

重入鎖有:

  • synchronized
  • ReentrantLock
  • ReentrantReadWriteLock

兩者區別

  • synchronized是獨佔鎖,加鎖和解鎖的過程自動進行,易於操作,但不夠靈活。ReentrantLock也是獨佔鎖,加鎖和解鎖的過程需要手動進行,不易操作,但非常靈活。
  • synchronized可重入,因爲加鎖和解鎖自動進行,不必擔心最後是否釋放鎖;ReentrantLock也可重入,但加鎖和解鎖需要手動進行,且次數需一樣,否則其他線程無法獲得鎖。
  • synchronized不可響應中斷,一個線程獲取不到鎖就一直等着;ReentrantLock可以相應中斷。

ReentrantLock

ReentrantLock的內部類Sync繼承了AQS,分爲公平鎖FairSync和非公平鎖NonfairSync。

Lock lock=new ReentrantLock();//默認非公平鎖
Lock lock=new ReentrantLock(true);//公平鎖
Lock lock=new ReentrantLock(false);//非公平鎖
getHoldCount() 查詢當前線程保持此鎖的次數,也就是執行此線程執行lock方法的次數。

getQueueLength()返回正等待獲取此鎖的線程數,比如啓動10個線程,1個線程獲得鎖,此時返回的是9。

getWaitQueueLength(Condition condition)返回在該條件隊列的線程數。

hasWaiters(Condition condition) 查詢該條件隊列是否有等待線程。

hasQueuedThread(Thread thread) 查詢指定的線程是否正在等待獲取Lock鎖。

hasQueuedThreads() 是否有線程等待此鎖

isFair() 該鎖是否公平鎖

isHeldByCurrentThread() 當前線程是否保持鎖鎖定,線程的執行lock方法的前後分別是false和true

isLock() 此鎖是否有任意線程佔用

lockInterruptibly() 如果當前線程未被中斷,獲取鎖

tryLock() 線程嘗試獲取鎖,如果獲取成功,返回 true,否則返回 false

tryLock(long timeout,TimeUnit unit) 線程如果在指定等待時間內獲得了鎖,就返回true,否則返回 false

ReentrantReadWriteLock

Java併發包中ReadWriteLock是一個接口,主要有兩個方法,如下:

public interface ReadWriteLock {
    //返回讀鎖
    Lock readLock();
    
    //返回寫鎖
    Lock writeLock();
}

ReetrantReadWriteLock實現了ReadWriteLock接口並添加了可重入的特性。

有兩個鎖,一個是讀操作相關的鎖,稱爲共享鎖;一個是寫相關的鎖,稱爲排他鎖。

讀寫鎖的機制:

  • "讀-讀" 不互斥
  • "讀-寫" 互斥
  • "寫-寫" 互斥

線程進入讀鎖的前提條件:

  • 沒有其他線程的寫鎖或者有寫請求,但調用線程和持有鎖的線程是同一個。

線程進入寫鎖的前提條件:

  • 沒有其他線程的讀鎖
  • 沒有其他線程的寫鎖

3個特性:

  • 公平選擇性:支持非公平(默認)和公平的鎖獲取方式,吞吐量還是非公平優於公平。
  • 重進入:讀鎖和寫鎖都支持線程重進入。
  • 鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成爲讀鎖。不支持鎖升級。

 

 

 

 

 

 

 

 

 

 

 

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