天天看點

解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 擷取鎖AQS算法 (百度-美團)

 本篇文章主要講了lock的原理 就是AQS算法,還有個姊妹篇 講解synchronized的實作原理 也是阿裡經常問的,

參考:深入分析Synchronized原理(阿裡面試題)

一定要看後面的文章,先說結論: 

非公平鎖tryAcquire的流程是:檢查state字段,若為0,表示鎖未被占用,那麼嘗試占用,若不為0,檢查目前鎖是否被自己占用,若被自己占用,則更新state字段,表示重入鎖的次數。如果以上兩點都沒有成功,則擷取鎖失敗,傳回false。

還有其他的鎖,如果想要了解,參考:JAVA鎖機制-可重入鎖,可中斷鎖,公平鎖,讀寫鎖,自旋鎖,

用synchronized實作ReentrantLock 美團面試題參考:使用synchronized 實作ReentrantLock(美團面試題目)

前幾天去百度面試,面試官問多線程如何解決并發問題,感覺自己對lock的原理了解不夠,這裡對兩種方式synchronized和lock做個系統的總結:

解決多線程的并發安全問題,java無非就是加鎖,具體就是兩個方法

(1) Synchronized(java自帶的關鍵字)

(2) lock 可重入鎖 (可重入鎖這個包java.util.concurrent.locks 底下有兩個接口,分别對應兩個類實作了這個兩個接口: 

       (a)lock接口, 實作的類為:ReentrantLock類 可重入鎖;

       (b)readwritelock接口,實作類為:ReentrantReadWriteLock 讀寫鎖)

也就是說有三種:

(1)synchronized 是互斥鎖;

(2)ReentrantLock 顧名思義 :可重入鎖

(3)ReentrantReadWriteLock :讀寫鎖

讀寫鎖特點:

a)多個讀者可以同時進行讀

b)寫者必須互斥(隻允許一個寫者寫,也不能讀者寫者同時進行)

c)寫者優先于讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)

總結來說,Lock和synchronized有以下幾點不同:

1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是内置的語言實作;

2)當synchronized塊結束時,會自動釋放鎖,lock一般需要在finally中自己釋放。synchronized在發生異常時,會自動釋放線程占有的鎖,是以不會導緻死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,是以使用Lock時需要在finally塊中釋放鎖;

3)lock等待鎖過程中可以用interrupt來終端等待,而synchronized隻能等待鎖的釋放,不能響應中斷。

4)lock可以通過trylock來知道有沒有擷取鎖,而synchronized不能; 

5. 當synchronized塊執行時,隻能使用非公平鎖,無法實作公平鎖,而lock可以通過new ReentrantLock(true)設定為公平鎖,進而在某些場景下提高效率。

6、LLock可以提高多個線程進行讀操作的效率。(可以通過readwritelock實作讀寫分離)

7、synchronized 鎖類型 可重入 不可中斷 非公平 而 lock 是: 可重入 可判斷 可公平(兩者皆可) 

在性能上來說,如果競争資源不激烈,兩者的性能是差不多的,而當競争資源非常激烈時(即有大量線程同時競争),此時Lock的性能要遠遠優于synchronized。是以說,在具體使用時要根據适當情況選擇。 

首先看一下Synchronized的原理:

1、synchronized

把代碼塊聲明為 synchronized,有兩個重要後果,通常是指該代碼具有 原子性(atomicity)和 可見性(visibility)。

實作原子性的算範為CAS(Compare and Swap) 參考:Java多線程系列——原子類的實作(CAS算法)

(1) 原子性

原子性意味着個時刻,隻有一個線程能夠執行一段代碼,這段代碼通過一個monitor object保護。進而防止多個線程在更新共享狀态時互相沖突。

 (2)  可見性

可見性則更為微妙,它要對付記憶體緩存和編譯器優化的各種反常行為。啥是可見性呢?

答:它必須確定釋放鎖之前對共享資料做出的更改對于随後獲得該鎖的另一個線程是可見的 。

作用:如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一緻的值,這将引發許多嚴重問題。 

一般來說,線程以某種不必讓其他線程立即可以看到的方式(不管這些線程在寄存器中、在處理器特定的緩存中,還是通過指令重排或者其他編譯器優化),不受緩存變量值的限制,但是如果開發人員使用了同步,那麼運作庫将確定某一線程對變量所做的更新先于對現有

synchronized

 塊所進行的更新,當進入由同一監控器(lock)保護的另一個

synchronized

 塊時,将立刻可以看到這些對變量所做的更新。類似的規則也存在于

volatile

變量上。

——volatile隻保證可見性,不保證原子性! 

(3)synchronize的限制:

  1. 當線程嘗試擷取鎖的時候,如果擷取不到鎖會一直阻塞, 它無法中斷一個正在等候獲得鎖的線程;
  2. 如果擷取鎖的線程進入休眠或者阻塞,除非目前線程異常,否則其他線程嘗試擷取鎖必須一直等待,也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖。

2、ReentrantLock (可重入鎖) 

何為可重入(美團面試提問過此處):參考:如何了解ReentrantLock的可重入和互斥?

可重入的意思是某一個線程是否可多次獲得一個鎖,在繼承的情況下,如果不是可重入的,那就形成死鎖了,比如遞歸調用自己的時候;,如果不能可重入,每次都擷取鎖不合适,比如synchronized就是可重入的,ReentrantLock也是可重入的

鎖的概念就不用多解釋了,當某個線程A已經持有了一個鎖,當線程B嘗試進入被這個鎖保護的代碼段的時候.就會被阻塞.而鎖的操作粒度是”線程”,而不是調用(至于為什麼要這樣,下面解釋).同一個線程再次進入同步代碼的時候.可以使用自己已經擷取到的鎖,這就是可重入鎖java裡面内置鎖(synchronize)和Lock(ReentrantLock)都是可重入的 

我自己寫了個例子:   

解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 擷取鎖AQS算法 (百度-美團)
解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 擷取鎖AQS算法 (百度-美團)
package entrantlock_test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class parent {
    
     protected Lock lock=new ReentrantLock();
     
     public void test(){
         lock.lock();
         try{
             System.out.println("Parent");
         }finally{
             lock.unlock();
         }
     }
     
     
}

class Sub extends parent{

    @Override
    public void test() {
        // TODO Auto-generated method stub
        lock.lock();
        try{
        super.test();
        System.out.println("Sub");
        
        }finally{
            lock.unlock();
        }
    }
    
    
}

public class LockTest{
    
    public static void main(String[] args){
        
        Sub s=new Sub();
        s.test();
        
    }
}      

View Code

 2.1 . 為什麼要可重入 

如果線程A繼續再次獲得這個鎖呢?比如一個方法是synchronized,遞歸調用自己,那麼第一次已經獲得了鎖,第二次調用的時候還能進入嗎? 直覺上當然需要能進入.這就要求必須是可重入的.可重入鎖又叫做遞歸鎖,不然就死鎖了。 

 它實作方式是:

為每個鎖關聯一個擷取計數器和一個所有者線程,當計數值為0的時候,這個所就沒有被任何線程隻有.當線程請求一個未被持有的鎖時,JVM将記下鎖的持有者,并且将擷取計數值置為1,如果同一個線程再次擷取這個鎖,技術值将遞增,退出一次同步代碼塊,計算值遞減,當計數值為0時,這個鎖就被釋放.ReentrantLock裡面有實作

其實也有不可重入鎖:這個還真有.Linux下的pthread_mutex_t鎖是預設是非遞歸的。可以通過設定PTHREAD_MUTEX_RECURSIVE屬性,将pthread_mutex_t鎖設定為遞歸鎖。如果要自己實作不可重入鎖,同可重入鎖,這個計數器隻能為1.或者0,再次進入的時候,發現已經是1了,就進行阻塞.jdk裡面沒有預設的實作類.

Java.util.concurrent.lock

 中的

Lock

 架構是鎖定的一個抽象,Lock彌補了synchronized的局限,提供了更加細粒度的加鎖功能。  

ReentrantLock

 類是唯一實作了

Lock的類

 ,它擁有與

synchronized

 相同的并發性和記憶體語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈争用情況下更佳的性能。(換句話說,當許多線程都想通路共享資源時,JVM 可以花更少的時候來排程線程,把更多時間用在執行線程上。)  

用sychronized修飾的方法或者語句塊在代碼執行完之後鎖自動釋放,而是用Lock需要我們手動釋放鎖,是以為了保證鎖最終被釋放(發生異常情況),要把互斥區放在try内,釋放鎖放在finally内!!  

Lock 接口api如下  

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

 lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來擷取鎖的。

unLock()方法是用來釋放鎖的。

在Lock中聲明了四個方法來擷取鎖,那麼這四個方法有何差別呢?

  首先lock()方法是平常使用得最多的一個方法,就是用來擷取鎖。如果鎖已被其他線程擷取,則進行等待。

  由于在前面講到如果采用Lock,必須主動去釋放鎖,并且在發生異常時,不會自動釋放鎖。是以一般來說,使用Lock必須在try{}catch{}塊中進行,并且将釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的: 

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

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

     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 {
//如果不能擷取鎖,則直接做其他事情
}      

   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修飾的話,當一個線程處于等待某個鎖的狀态,是無法被中斷的,隻有一直等待下去。   

2 AQS

    AbstractQueuedSynchronizer簡稱AQS,是一個用于建構鎖和同步容器的架構。事實上concurrent包内許多類都是基于AQS建構,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解決了在實作同步容器時設計的大量細節問題。

    AQS使用一個FIFO的隊清單示排隊等待鎖的線程,它維護一個status的變量,每個節點維護一個waitstatus的變量,當線程擷取到鎖的時候,隊列的status置為1,此線程執行完了,那麼它的waitstatus為-1;隊列頭部的線程執行完畢之後,它會調用它的後繼的線程(百度面試)。

隊列頭節點稱作“哨兵節點”或者“啞節點”,它不與任何線程關聯。其他的節點與等待線程關聯,每個節點維護一個等待狀态waitStatus。如圖

解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 擷取鎖AQS算法 (百度-美團)

     AQS中還有一個表示狀态的字段state,例如ReentrantLocky用它表示線程重入鎖的次數,Semaphore用它表示剩餘的許可數量,FutureTask用它表示任務的狀态。對state變量值的更新都采用CAS操作保證更新操作的原子性。

    AbstractQueuedSynchronizer繼承了AbstractOwnableSynchronizer,這個類隻有一個變量:exclusiveOwnerThread,表示目前占用該鎖的線程,并且提供了相應的get,set方法。

    了解AQS可以幫助我們更好的了解JCU包中的同步容器。

3 lock()與unlock()實作原理

        ReentrantLock是Lock的預設實作之一。那麼lock()和unlock()是怎麼實作的呢?首先我們要弄清楚幾個概念

  • 可重入鎖。可重入鎖是指同一個線程可以多次擷取同一把鎖。ReentrantLock和synchronized都是可重入鎖。
  • 可中斷鎖。可中斷鎖是指線程嘗試擷取鎖的過程中,是否可以響應中斷。synchronized是不可中斷鎖,而ReentrantLock則提供了中斷功能。
  • 公平鎖與非公平鎖。公平鎖是指多個線程同時嘗試擷取同一把鎖時,擷取鎖的順序按照線程達到的順序,而非公平鎖則允許線程“插隊”。synchronized是非公平鎖,而ReentrantLock的預設實作是非公平鎖,但是也可以設定為公平鎖。
  • CAS操作(CompareAndSwap)。CAS操作簡單的說就是比較并交換。CAS 操作包含三個操作數 —— 記憶體位置(V)、預期原值(A)和新值(B)。如果記憶體位置的值與預期原值相比對,那麼處理器會自動将該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前傳回該位置的值。CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則将 B 放到這個位置;否則,不要更改該位置,隻告訴我這個位置現在的值即可。” Java并發包(java.util.concurrent)中大量使用了CAS操作,涉及到并發的地方都調用了sun.misc.Unsafe類方法進行CAS操作。

    ReentrantLock提供了兩個構造器,分别是 

public ReentrantLock() {
    sync = new NonfairSync();
}
 
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}      

    預設構造器初始化為NonfairSync對象,即非公平鎖,而帶參數的構造器可以指定使用公平鎖和非公平鎖。由lock()和unlock的源碼可以看到,它們隻是分别調用了sync對象的lock()和release(1)方法。

    Sync是ReentrantLock的内部類,它的結構如下

解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 擷取鎖AQS算法 (百度-美團)

 可以看到Sync擴充了AbstractQueuedSynchronizer。

3.3 NonfairSync

    我們從源代碼出發,分析非公平鎖擷取鎖和釋放鎖的過程。 

3.3.1 lock() 

    lock()源碼如下 

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}      

      首先用一個CAS操作,判斷state是否是0(表示目前鎖未被占用),如果是0則把它置為1,并且設定目前線程為該鎖的獨占線程,表示擷取鎖成功。當多個線程同時嘗試占用同一個鎖時,CAS操作隻能保證一個線程操作成功,剩下的隻能乖乖的去排隊啦。

    “非公平”即展現在這裡,如果占用鎖的線程剛釋放鎖,state置為0,而排隊等待鎖的線程還未喚醒時,新來的線程就直接搶占了該鎖,那麼就“插隊”了(請注意此處的非公平鎖是指新來的線程跟隊列頭部的線程競争鎖,隊列其他的線程還是正常排隊,百度面試題)。

    若目前有三個線程去競争鎖,假設線程A的CAS操作成功了,拿到了鎖開開心心的傳回了,那麼線程B和C則設定state失敗,走到了else裡面。我們往下看acquire。

acquire(arg)

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}      

代碼非常簡潔,但是背後的邏輯卻非常複雜,可見Doug Lea大神的程式設計功力。

 1. 第一步。嘗試去擷取鎖。如果嘗試擷取鎖成功,方法直接傳回。

tryAcquire(arg) 

final boolean nonfairTryAcquire(int acquires) {
    //擷取目前線程
    final Thread current = Thread.currentThread();
    //擷取state變量值
    int c = getState();
    if (c == 0) { //沒有線程占用鎖
        if (compareAndSetState(0, acquires)) {
            //占用鎖成功,設定獨占線程為目前線程
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { //目前線程已經占用該鎖
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 更新state值為新的重入次數
        setState(nextc);
        return true;
    }
    //擷取鎖失敗
    return false;
}      

    非公平鎖tryAcquire的流程是:檢查state字段,若為0,表示鎖未被占用,那麼嘗試占用,若不為0,檢查目前鎖是否被自己占用,若被自己占用,則更新state字段,表示重入鎖的次數。如果以上兩點都沒有成功,則擷取鎖失敗,傳回false。

2. 第二步,入隊。由于上文中提到線程A已經占用了鎖,是以B和C執行tryAcquire失敗,并且入等待隊列。如果線程A拿着鎖死死不放,那麼B和C就會被挂起。

先看下入隊的過程。

先看addWaiter(Node.EXCLUSIVE) 

/**
 * 将新節點和目前線程關聯并且入隊列
 * @param mode 獨占/共享
 * @return 新節點
 */
private Node addWaiter(Node mode) {
    //初始化節點,設定關聯線程和模式(獨占 or 共享)
    Node node = new Node(Thread.currentThread(), mode);
    // 擷取尾節點引用
    Node pred = tail;
    // 尾節點不為空,說明隊列已經初始化過
    if (pred != null) {
        node.prev = pred;
        // 設定新節點為尾節點
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 尾節點為空,說明隊列還未初始化,需要初始化head節點并入隊新節點
    enq(node);
    return node;
}      

 B、C線程同時嘗試入隊列,由于隊列尚未初始化,tail==null,故至少會有一個線程會走到enq(node)。我們假設同時走到了enq(node)裡。 

/**
 * 初始化隊列并且入隊新節點
 */
private Node enq(final Node node) {
    //開始自旋
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            // 如果tail為空,則建立一個head節點,并且tail指向head
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            // tail不為空,将新節點入隊
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}      

這裡展現了經典的自旋+CAS組合來實作非阻塞的原子操作。由于compareAndSetHead的實作使用了unsafe類提供的CAS操作,是以隻有一個線程會建立head節點成功。假設線程B成功,之後B、C開始第二輪循環,此時tail已經不為空,兩個線程都走到else裡面。假設B線程compareAndSetTail成功,那麼B就可以傳回了,C由于入隊失敗還需要第三輪循環。最終所有線程都可以成功入隊。

     當B、C入等待隊列後,此時AQS隊列如下:

解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 擷取鎖AQS算法 (百度-美團)

3. 第三步,挂起。B和C相繼執行acquireQueued(final Node node, int arg)。這個方法讓已經入隊的線程嘗試擷取鎖,若失敗則會被挂起。 

/**
 * 已經入隊的線程嘗試擷取鎖
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; //标記是否成功擷取鎖
    try {
        boolean interrupted = false; //标記線程是否被中斷過
        for (;;) {
            final Node p = node.predecessor(); //擷取前驅節點
            //如果前驅是head,即該結點已成老二,那麼便有資格去嘗試擷取鎖
            if (p == head && tryAcquire(arg)) {
                setHead(node); // 擷取成功,将目前節點設定為head節點
                p.next = null; // 原head節點出隊,在某個時間點被GC回收
                failed = false; //擷取成功
                return interrupted; //傳回是否被中斷過
            }
            // 判斷擷取失敗後是否可以挂起,若可以則挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                // 線程若被中斷,設定interrupted為true
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}      

code裡的注釋已經很清晰的說明了acquireQueued的執行流程。假設B和C在競争鎖的過程中A一直持有鎖,那麼它們的tryAcquire操作都會失敗,是以會走到第2個if語句中。我們再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了哪些事吧。 

/**
 * 判斷目前線程擷取鎖失敗之後是否需要挂起.
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //前驅節點的狀态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 前驅節點狀态為signal,傳回true
        return true;
    // 前驅節點狀态為CANCELLED
    if (ws > 0) {
        // 從隊尾向前尋找第一個狀态不為CANCELLED的節點
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 将前驅節點的狀态設定為SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  
/**
 * 挂起目前線程,傳回線程中斷狀态并重置
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}      

    線程入隊後能夠挂起的前提是,它的前驅節點的狀态為SIGNAL,它的含義是“Hi,前面的兄弟,如果你擷取鎖并且出隊後,記得把我喚醒!”。是以shouldParkAfterFailedAcquire會先判斷目前節點的前驅是否狀态符合要求,若符合則傳回true,然後調用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驅節點是否>0(CANCELLED),若是那麼向前周遊直到找到第一個符合要求的前驅,若不是則将前驅節點的狀态設定為SIGNAL。

也就是說當隊列頭部的線程執行完了之後,這個線程會調用後面的隊列的第一個線程(百度面試)。

     整個流程中,如果前驅結點的狀态不是SIGNAL,那麼自己就不能安心挂起,需要去找個安心的挂起點,同時可以再嘗試下看有沒有機會去嘗試競争鎖。

    最終隊列可能會如下圖所示

解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 擷取鎖AQS算法 (百度-美團)

  線程B和C都已經入隊,并且都被挂起。當線程A釋放鎖的時候,就會去喚醒線程B去擷取鎖啦。

3.3.2 unlock()

unlock相對于lock就簡單很多。源碼如下 

public void unlock() {
    sync.release(1);
}
  
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}      

如果了解了加鎖的過程,那麼解鎖看起來就容易多了。流程大緻為先嘗試釋放鎖,若釋放成功,那麼檢視頭結點的狀态是否為SIGNAL,如果是則喚醒頭結點的下個節點關聯的線程,如果釋放失敗那麼傳回false表示解鎖失敗。這裡我們也發現了,每次都隻喚起頭結點的下一個節點關聯的線程。

   最後我們再看下tryRelease的執行過程 

/**
 * 釋放目前線程占用的鎖
 * @param releases
 * @return 是否釋放成功
 */
protected final boolean tryRelease(int releases) {
    // 計算釋放後state值
    int c = getState() - releases;
    // 如果不是目前線程占用鎖,那麼抛出異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 鎖被重入次數為0,表示釋放成功
        free = true;
        // 清空獨占線程
        setExclusiveOwnerThread(null);
    }
    // 更新state值
    setState(c);
    return free;
}      

這裡入參為1。tryRelease的過程為:目前釋放鎖的線程若不持有鎖,則抛出異常。若持有鎖,計算釋放後的state值是否為0,若為0表示鎖已經被成功釋放,并且則清空獨占線程,最後更新state值,傳回free。 

3.3.3 小結

    用一張流程圖總結一下非公平鎖的擷取鎖的過程。    

解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 擷取鎖AQS算法 (百度-美團)

3.4 FairSync

    公平鎖和非公平鎖不同之處在于,公平鎖在擷取鎖的時候,不會先去檢查state狀态,而是直接執行aqcuire(1),這裡不再贅述。    

4 逾時機制

    在ReetrantLock的tryLock(long timeout, TimeUnit unit) 提供了逾時擷取鎖的功能。它的語義是在指定的時間内如果擷取到鎖就傳回true,擷取不到則傳回false。這種機制避免了線程無限期的等待鎖釋放。那麼逾時的功能是怎麼實作的呢?我們還是用非公平鎖為例來一探究竟。

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}      

 還是調用了内部類裡面的方法。我們繼續向前探究  

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}      

這裡的語義是:如果線程被中斷了,那麼直接抛出InterruptedException。如果未中斷,先嘗試擷取鎖,擷取成功就直接傳回,擷取失敗則進入doAcquireNanos。tryAcquire我們已經看過,這裡重點看一下doAcquireNanos做了什麼。 

/**
 * 在有限的時間内去競争鎖
 * @return 是否擷取成功
 */
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 起始時間
    long lastTime = System.nanoTime();
    // 線程入隊
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        // 又是自旋!
        for (;;) {
            // 擷取前驅節點
            final Node p = node.predecessor();
            // 如果前驅是頭節點并且占用鎖成功,則将目前節點變成頭結點
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            // 如果已經逾時,傳回false
            if (nanosTimeout <= 0)
                return false;
            // 逾時時間未到,且需要挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                // 阻塞目前線程直到逾時時間到期
                LockSupport.parkNanos(this, nanosTimeout);
            long now = System.nanoTime();
            // 更新nanosTimeout
            nanosTimeout -= now - lastTime;
            lastTime = now;
            if (Thread.interrupted())
                //相應中斷
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}      

doAcquireNanos的流程簡述為:線程先入等待隊列,然後開始自旋,嘗試擷取鎖,擷取成功就傳回,失敗則在隊列裡找一個安全點把自己挂起直到逾時時間過期。這裡為什麼還需要循環呢?因為目前線程節點的前驅狀态可能不是SIGNAL,那麼在目前這一輪循環中線程不會被挂起,然後更新逾時時間,開始新一輪的嘗試 

3、讀寫鎖ReentrantReadWriteLock

接口 ReadWriteLock,有個實作類是ReentrantReadWriteLock

讀讀互不幹擾,寫寫互斥,如果有讀也有寫,那麼寫線程要優先讀線程

對!讀取線程不應該互斥!

我們可以用讀寫鎖ReadWriteLock實作:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; 
class Data {        
    private int data;// 共享資料    
    private ReadWriteLock rwl = new ReentrantReadWriteLock();       
    public void set(int data) {    
        rwl.writeLock().lock();// 取到寫鎖    
        try {    
            System.out.println(Thread.currentThread().getName() + "準備寫入資料");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            this.data = data;    
            System.out.println(Thread.currentThread().getName() + "寫入" + this.data);    
        } finally {    
            rwl.writeLock().unlock();// 釋放寫鎖    
        }    
    }       
  
    public void get() {    
        rwl.readLock().lock();// 取到讀鎖    
        try {    
            System.out.println(Thread.currentThread().getName() + "準備讀取資料");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            System.out.println(Thread.currentThread().getName() + "讀取" + this.data);    
        } finally {    
            rwl.readLock().unlock();// 釋放讀鎖    
        }    
    }    
}        

與互斥鎖定相比,讀-寫鎖定允許對共享資料進行更進階别的并發通路。雖然一次隻有一個線程(writer 線程)可以修改共享資料,但在許多情況下,任何數量的線程可以同時讀取共享資料(reader 線程) 

從理論上講,與互斥鎖定相比,使用讀-寫鎖定所允許的并發性增強将帶來更大的性能提高。

在實踐中,隻有在多處理器上并且隻在通路模式适用于共享資料時,才能完全實作并發性增強。——例如,某個最初用資料填充并且之後不經常對其進行修改的 collection,因為經常對其進行搜尋(比如搜尋某種目錄),是以這樣的 collection 是使用讀-寫鎖定的理想候選者。 

4、線程間通信Condition

Condition可以替代傳統的線程間通信,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll()。

——為什麼方法名不直接叫wait()/notify()/nofityAll()?因為Object的這幾個方法是final的,不可重寫!

傳統線程的通信方式,Condition都可以實作。

注意,Condition是被綁定到Lock上的,要建立一個Lock的Condition必須用newCondition()方法。 

Condition的強大之處在于它可以為多個線程間建立不同的Condition

看JDK文檔中的一個例子:假定有一個綁定的緩沖區,它支援 put 和 take 方法。如果試圖在空的緩沖區上執行take 操作,則在某一個項變得可用之前,線程将一直阻塞;如果試圖在滿的緩沖區上執行 put 操作,則在有空間變得可用之前,線程将一直阻塞。我們喜歡在單獨的等待 set 中儲存put 線程和take 線程,這樣就可以在緩沖區中的項或空間變得可用時利用最佳規劃,一次隻通知一個線程。可以使用兩個

Condition

 執行個體來做到這一點。

——其實就是java.util.concurrent.ArrayBlockingQueue的功能

優點:

假設緩存隊列中已經存滿,那麼阻塞的肯定是寫線程,喚醒的肯定是讀線程,相反,阻塞的肯定是讀線程,喚醒的肯定是寫線程。 

如果想檢視 線程5個狀态 請參考:Java線程的5種狀态及切換(透徹講解)-京東面試

以下是補充的知識點:

1、線程與程序:

在開始之前先把程序與線程進行區分一下,一個程式最少需要一個程序,而一個程序最少需要一個線程。

線程是程式執行流的最小機關,而程序是系統進行資源配置設定和排程的一個獨立機關。  

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

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的差別。 

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

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

參考:  lock與synchronized差別詳解

參考:  Synchronized與Lock的差別與應用場景

參考:lock和synchronized的同步差別與選擇 

參考:ReentrantLock實作原理

解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 擷取鎖AQS算法 (百度-美團)
解決多線程安全問題-無非兩個方法synchronized和lock 具體原理以及如何 擷取鎖AQS算法 (百度-美團)
package entrantlock_test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class parent {
    
     protected Lock lock=new ReentrantLock();
     
     public void test(){
         lock.lock();
         try{
             System.out.println("Parent");
         }finally{
             lock.unlock();
         }
     }
     
     
}

class Sub extends parent{

    @Override
    public void test() {
        // TODO Auto-generated method stub
        lock.lock();
        try{
        super.test();
        System.out.println("Sub");
        
        }finally{
            lock.unlock();
        }
    }
    
    
}

public class LockTest{
    
    public static void main(String[] args){
        
        Sub s=new Sub();
        s.test();
        
    }
}