Synchronized關鍵字與ReetrantLock同步鎖學習筆記

一、Synchronized

1、概念

synchronized保證被修飾的方法或代碼塊操作的原子性、可見性和有序性。synchronized可重入鎖、不可中斷,適合線程競爭不激烈。

  1. 被Synchronized 關鍵字描述的方法或代碼塊在多線程環境下同一時間只能由一個線程進行訪問,因爲在持有當前鎖的線程執行完成之前,其他線程想要調用相關方法就必須進行排隊,直到當前線程執行完成才釋放鎖給其他線程,所以保證了原子性。
  2. 被Synchronized 關鍵字描述的方法或代碼塊在多線程環境下數據是同步的,即當獲取到鎖後先將內存複製到自己的緩存中操作,釋放鎖之前會把緩存中的數據複製到共享內存中,所以保證了可見性。
  3. 被Synchronized 關鍵字描述的方法或代碼塊在多線程環境下同一時間只能由一個線程訪問,代碼內部是有序的,不會出發JMM指令重排機制,所以保證了有序性。

在使用Sychronized關鍵字時需要把握如下注意點:

  • 一把鎖只能同時被一個線程獲取,沒有獲得鎖的線程只能等待;
  • 每個實例都對應有自己的一把鎖(this),不同實例之間互不影響;例外:鎖對象是*.class以及synchronized修飾的是static方法的時候,所有對象公用同一把鎖
  • synchronized修飾的方法,無論方法正常執行完畢還是拋出異常,都會釋放鎖

2、對象鎖

包括方法鎖(默認鎖對象爲this,當前實例對象)和同步代碼塊鎖(自己指定鎖對象)

代碼塊形式
手動指定鎖定對象,也可是是this,也可以是自定義的鎖

  • 示例1
 public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();

    @Override
    public void run() {
        // 同步代碼塊形式——鎖爲this,兩個線程使用的鎖是一樣的,線程1必須要等到線程0釋放了該鎖後,才能執行
        synchronized (this) {
            System.out.println("我是線程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "結束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

輸出結果:

我是線程Thread-0
Thread-0結束
我是線程Thread-1
Thread-1結束
  • 示例2
public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();
    // 創建2把鎖
    Object block1 = new Object();
    Object block2 = new Object();

    @Override
    public void run() {
        // 這個代碼塊使用的是第一把鎖,當他釋放後,後面的代碼塊由於使用的是第二把鎖,因此可以馬上執行
        synchronized (block1) {
            System.out.println("block1鎖,我是線程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block1鎖,"+Thread.currentThread().getName() + "結束");
        }

        synchronized (block2) {
            System.out.println("block2鎖,我是線程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block2鎖,"+Thread.currentThread().getName() + "結束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

輸出結果:

block1鎖,我是線程Thread-0
block1鎖,Thread-0結束
block2鎖,我是線程Thread-0  // 可以看到當第一個線程在執行完第一段同步代碼塊之後,第二個同步代碼塊可以馬上得到執行,因爲他們使用的鎖不是同一把
block1鎖,我是線程Thread-1
block2鎖,Thread-0結束
block1鎖,Thread-1結束
block2鎖,我是線程Thread-1
block2鎖,Thread-1結束

方法鎖形式:
synchronized修飾普通方法,鎖對象默認爲this

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();

    @Override
    public void run() {
        method();
    }

    public synchronized void method() {
        System.out.println("我是線程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

輸出結果:

我是線程Thread-0
Thread-0結束
我是線程Thread-1
Thread-1結束

3、類鎖

指synchronize修飾靜態的方法或指定鎖對象爲Class對象
synchronize修飾靜態方法

  • 示例1
public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
    static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();

    @Override
    public void run() {
        method();
    }

    // synchronized用在普通方法上,默認的鎖就是this,當前實例
    public synchronized void method() {
        System.out.println("我是線程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束");
    }

    public static void main(String[] args) {
        // t1和t2對應的this是兩個不同的實例,所以代碼不會串行
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

輸出結果:

我是線程Thread-0
我是線程Thread-1
Thread-1結束
Thread-0結束
  • 示例2
public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
    static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();

    @Override
    public void run() {
        method();
    }

    // synchronized用在靜態方法上,默認的鎖就是當前所在的Class類,所以無論是哪個線程訪問它,需要的鎖都只有一把
    public static synchronized void method() {
        System.out.println("我是線程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

輸出結果:

我是線程Thread-0
Thread-0結束
我是線程Thread-1
Thread-1結束

synchronized指定鎖對象爲Class對象

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
    static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();

    @Override
    public void run() {
        // 所有線程需要的鎖都是同一把
        synchronized(SynchronizedObjectLock.class){
            System.out.println("我是線程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "結束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}

輸出結果:

我是線程Thread-0
Thread-0結束
我是線程Thread-1
Thread-1結束

4、加鎖和釋放鎖的原理

深入JVM看字節碼,創建如下的代碼:

public class SynchronizedDemo2 {
    Object object = new Object();
    public void method1() {
        synchronized (object) {

        }
    }
}

使用javac命令進行編譯生成.class文件

>javac SynchronizedDemo2.java

使用javap命令反編譯查看.class文件的信息

>javap -verbose SynchronizedDemo2.class

得到如下的信息:
在這裏插入圖片描述

關注紅色方框裏的monitorentermonitorexit即可。

Monitorenter和Monitorexit指令,會讓對象在執行,使其鎖計數器加1或者減1。每一個對象在同一時間只與一個monitor(鎖)相關聯,而一個monitor在同一時間只能被一個線程獲得,一個對象在嘗試獲得與這個對象相關聯的Monitor鎖的所有權的時候,monitorenter指令會發生如下3中情況之一:

  1. monitor計數器爲0,意味着目前還沒有被獲得,那這個線程就會立刻獲得然後把鎖計數器+1,一旦+1,別的線程再想獲取,就需要等待
  2. 如果這個monitor已經拿到了這個鎖的所有權,又重入了這把鎖,那鎖計數器就會累加,變成2,並且隨着重入的次數,會一直累加,這就是可重入機制。
  3. 這把鎖已經被別的線程獲取了,等待鎖釋放

monitorexit指令:釋放對於monitor的所有權,釋放過程很簡單,就是講monitor的計數器減1,如果減完以後,計數器不是0,則代表剛纔是重入進來的,當前線程還繼續持有這把鎖的所有權,如果計數器變成0,則代表當前線程不再擁有該monitor的所有權,即釋放鎖。

下圖表現了對象,對象監視器,同步隊列以及執行線程狀態之間的關係:
在這裏插入圖片描述
該圖可以看出,任意線程對Object的訪問,首先要獲得Object的監視器,如果獲取失敗,該線程就進入同步狀態,線程狀態變爲BLOCKED,當Object的監視器佔有者釋放後,在同步隊列中得線程就會有機會重新獲取該監視器。

5、保證可見性的原理:內存模型和happens-before規則

Synchronized的happens-before規則,即監視器鎖規則:對同一個監視器的解鎖,happens-before於對該監視器的加鎖。繼續來看代碼:

public class MonitorDemo {
    private int a = 0;

    public synchronized void writer() {     // 1
        a++;                                // 2
    }                                       // 3

    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6
}

該代碼的happens-before關係如圖所示:
在這裏插入圖片描述
在圖中每一個箭頭連接的兩個節點就代表之間的happens-before關係,黑色的是通過程序順序規則推導出來,紅色的爲監視器鎖規則推導而出:線程A釋放鎖happens-before線程B加鎖,藍色的則是通過程序順序規則和監視器鎖規則推測出來happens-befor關係,通過傳遞性規則進一步推導的happens-before關係。現在我們來重點關注2 happens-before 5,通過這個關係我們可以得出什麼?

根據happens-before的定義中的一條:如果A happens-before B,則A的執行結果對B可見,並且A的執行順序先於B。線程A先對共享變量A進行加一,由2 happens-before 5關係可知線程A的執行結果對線程B可見即線程B所讀取到的a的值爲1。

6、JVM中鎖的優化

簡單來說在JVM中monitorenter和monitorexit字節碼依賴於底層的操作系統的Mutex Lock來實現的,但是由於使用Mutex Lock需要將當前線程掛起並從用戶態切換到內核態來執行,這種切換的代價是非常昂貴的;然而在現實中的大部分情況下,同步方法是運行在單線程環境(無鎖競爭環境)如果每次都調用Mutex Lock那麼將嚴重的影響程序的性能。不過在jdk1.6中對鎖的實現引入了大量的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應性自旋(Adaptive Spinning)等技術來減少鎖操作的開銷。

  • 鎖粗化(Lock Coarsening):也就是減少不必要的緊連在一起的unlock,lock操作,將多個連續的鎖擴展成一個範圍更大的鎖。
  • 鎖消除(Lock Elimination):通過運行時JIT編譯器的逃逸分析來消除一些沒有在當前同步塊以外被其他線程共享的數據的鎖保護,通過逃逸分析也可以在線程本地Stack上進行對象空間的分配(同時還可以減少Heap上的垃圾收集開銷)。
  • 輕量級鎖(Lightweight Locking):這種鎖實現的背後基於這樣一種假設,即在真實的情況下我們程序中的大部分同步代碼一般都處於無鎖競爭狀態(即單線程執行環境),在無鎖競爭的情況下完全可以避免調用操作系統層面的重量級互斥鎖,取而代之的是在monitorenter和monitorexit中只需要依靠一條CAS原子指令就可以完成鎖的獲取及釋放。當存在鎖競爭的情況下,執行CAS指令失敗的線程將調用操作系統互斥鎖進入到阻塞狀態,當鎖被釋放的時候被喚醒(具體處理步驟下面詳細討論)。
  • 偏向鎖(Biased Locking):是爲了在無鎖競爭的情況下避免在鎖獲取過程中執行不必要的CAS原子指令,因爲CAS原子指令雖然相對於重量級鎖來說開銷比較小但還是存在非常可觀的本地延遲。
  • 適應性自旋(Adaptive Spinning):當線程在獲取輕量級鎖的過程中執行CAS操作失敗時,在進入與monitor相關聯的操作系統重量級鎖(mutex semaphore)前會進入忙等待(Spinning)然後再次嘗試,當嘗試一定的次數後如果仍然沒有成功則調用與該monitor關聯的semaphore(即互斥鎖)進入到阻塞狀態。

鎖的優缺點對比

優點 缺點 使用場景
偏向鎖 加鎖和解鎖不需要CAS操作,沒有額外的性能消耗,和執行非同步方法相比僅存在納秒級的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步快的場景
輕量級鎖 競爭的線程不會阻塞,提高了響應速度 如線程成始終得不到鎖競爭的線程,使用自旋會消耗CPU性能 追求響應時間,同步快執行速度非常快
重量級鎖 線程競爭不適用自旋,不會消耗CPU 線程阻塞,響應時間緩慢,在多線程下,頻繁的獲取釋放鎖,會帶來巨大的性能消耗 追求吞吐量,同步快執行速度較長

二、ReentrantLock

1、概念

ReentrantLock是一個可重入且獨佔式的鎖,它具有與使用synchronized監視器鎖相同的基本行爲和語義,但與synchronized關鍵字相比,它更靈活、更強大,增加了輪詢、超時、中斷等高級功能。ReentrantLock,顧名思義,它是支持可重入鎖的鎖,是一種遞歸無阻塞的同步機制。除此之外,該鎖還支持獲取鎖時的公平和非公平選擇。

2、源碼解析

ReentrantLock實現了Lock接口,Lock接口中定義了lock與unlock相關操作,並且還存在newCondition方法,表示生成一個條件。

public class ReentrantLock implements Lock, java.io.Serializable

ReentrantLock類的sync非常重要,對ReentrantLock類的操作大部分都直接轉化爲對Sync和AbstractQueuedSynchronizer類的操作。

  • 同步隊列
   
    private final Sync sync;
  • ReentrantLock()構造函數

默認是採用的非公平策略獲取鎖

public ReentrantLock() {
    // 默認非公平策略
    sync = new NonfairSync();
}
  • ReentrantLock(boolean)構造函數

可以傳遞參數確定採用公平策略或者是非公平策略,參數爲true表示公平策略,否則,採用非公平策略:

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

內部類
ReentrantLock總共有三個內部類,並且三個內部類是緊密相關的,下面先看三個類的關係。
在這裏插入圖片描述

ReentrantLock類內部總共存在Sync、NonfairSync、FairSync三個類,NonfairSync與FairSync類繼承自Sync類,Sync類繼承自AbstractQueuedSynchronizer抽象類。下面逐個進行分析。

  • Sync類

Sync類的源碼如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 序列號
    private static final long serialVersionUID = -5179523762034025860L;
    
    // 獲取鎖
    abstract void lock();
    
    // 非公平方式獲取
    final boolean nonfairTryAcquire(int acquires) {
        // 當前線程
        final Thread current = Thread.currentThread();
        // 獲取狀態
        int c = getState();
        if (c == 0) { // 表示沒有線程正在競爭該鎖
            if (compareAndSetState(0, acquires)) { // 比較並設置狀態爲 acquires 成功,狀態0表示鎖沒有被佔用
                // 設置當前線程獨佔
                setExclusiveOwnerThread(current); 
                return true; // 成功
            }
        }
        else if (current == getExclusiveOwnerThread()) { // 如果是當前線程擁有該鎖
            int nextc = c + acquires; // 增加重入次數
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 設置狀態
            setState(nextc); 
            // 成功
            return true; 
        }
        // 失敗
        return false;
    }
    // 試圖在共享模式下獲取對象狀態,此方法應該查詢是否允許它在共享模式下獲取對象狀態,如果允許,則獲取它
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread()) // 當前線程不爲獨佔線程
            throw new IllegalMonitorStateException(); // 拋出異常
        // 釋放標識
        boolean free = false; 
        if (c == 0) {
            free = true;
            // 已經釋放,清空獨佔
            setExclusiveOwnerThread(null); 
        }
        // 設置標識
        setState(c); 
        return free; 
    }
    // 判斷資源是否被當前線程佔有
    protected final boolean isHeldExclusively() {
        return getExclusiveOwnerThread() == Thread.currentThread();
    }
    // 新生一個條件
    final ConditionObject newCondition() {
        return new ConditionObject();
    }
    // 返回資源的佔用線程
    final Thread getOwner() {        
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }
    // 返回狀態
    final int getHoldCount() {            
        return isHeldExclusively() ? getState() : 0;
    }

    // 資源是否被佔用
    final boolean isLocked() {        
        return getState() != 0;
    }

    /**
        * Reconstitutes the instance from a stream (that is, deserializes it).
        */
    // 自定義反序列化邏輯
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}  

Sync類存在如下方法和作用如下。
在這裏插入圖片描述

  • NonfairSync類

NonfairSync類繼承了Sync類,表示採用非公平策略獲取鎖,其實現了Sync類中抽象的lock方法,源碼如下:

// 非公平鎖
static final class NonfairSync extends Sync {
    // 版本號
    private static final long serialVersionUID = 7316153563782823691L;

    // 獲得鎖
    final void lock() {
        if (compareAndSetState(0, 1)) // 比較並設置狀態成功,狀態0表示鎖沒有被佔用
            // 把當前線程設置獨佔了鎖
            setExclusiveOwnerThread(Thread.currentThread());
        else // 鎖已經被佔用,或者set失敗
            // 以獨佔模式獲取對象,忽略中斷
            acquire(1); 
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

說明: 從lock方法的源碼可知,每一次都嘗試獲取鎖,而並不會按照公平等待的原則進行等待,讓等待時間最久的線程獲得鎖。

  • FairSyn類
    FairSync類也繼承了Sync類,表示採用公平策略獲取鎖,其實現了Sync類中的抽象lock方法,源碼如下:
// 公平鎖
static final class FairSync extends Sync {
    // 版本序列化
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        // 以獨佔模式獲取對象,忽略中斷
        acquire(1);
    }
    
    // 嘗試公平獲取鎖
    protected final boolean tryAcquire(int acquires) {
        // 獲取當前線程
        final Thread current = Thread.currentThread();
        // 獲取狀態
        int c = getState();
        if (c == 0) { // 狀態爲0
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) { // 不存在已經等待更久的線程並且比較並且設置狀態成功
                // 設置當前線程獨佔
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { // 如果佔用鎖的線程爲當前線程。
            // 下一個狀態
            int nextc = c + acquires;
            if (nextc < 0) // 超過了int的表示範圍
                throw new Error("Maximum lock count exceeded");
            // 設置狀態
            setState(nextc);
            return true;
        }
        return false;
    }
}

說明: 跟蹤lock方法的源碼可知,當資源空閒時,它總是會先判斷sync隊列(AbstractQueuedSynchronizer中的數據結構)是否有等待時間更長的線程,如果存在,則將該線程加入到等待隊列的尾部,實現了公平獲取原則。其中,FairSync類的lock的方法調用如下,只給出了主要的方法。
在這裏插入圖片描述
說明: 可以看出只要資源被其他線程佔用,該線程就會添加到sync queue中的尾部,而不會先嚐試獲取資源。這也是和Nonfair最大的區別,Nonfair每一次都會嘗試去獲取資源,如果此時該資源恰好被釋放,則會被當前線程獲取,這就造成了不公平的現象,當獲取不成功,再加入隊列尾部。

3、獲取鎖

ReentrantLock lock=new ReentrantLock();
lock.lock();

1.lock()
上面代碼是我們常用的獲取鎖的方式:調用ReentrantLock 的lock()方法,默認情況下ReentrantLock是一個非公平鎖,類名NonfairSync,屬於ReentrantLock的內部類,我們來看源碼:

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

這裏的lock方法先去通過CAS操作將state的值從0變爲1,如果成功,就設置ExclusiveOwnerThread的值爲當前線程,ReentrantLock用exclusiveOwnerThread表示“持有鎖的線程”。如果設置失敗,說明state>0已經有線程持有了鎖,此時執行acquire(1)再請求一次鎖。

2.acquire()

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

acquire方法步驟大概是請求獨佔鎖,忽略所有中斷,至少執行一次tryAcquire,如果成功就返回,否則線程進入阻塞–喚醒兩種狀態切換中,直到tryAcquire成功。

我們對裏面的tryAcquire(),、addWaiter()、acquireQueued()挨個分析。
在這個方法裏先執行tryAcquire(arg):

3.tryAcquire()

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            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) 
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

先判斷state是否爲0,如果爲0就執行上面提到的lock方法的前半部分,通過CAS操作將state的值從0變爲1,否則判斷當前線程是否爲exclusiveOwnerThread,然後把state++,也就是重入鎖的體現,我們注意前半部分是通過CAS來保證同步,後半部分並沒有同步的體現,原因是:

後半部分是線程重入,再次獲得鎖時才觸發的操作,此時當前線程擁有鎖,所以對ReentrantLock的屬性操作是無需加鎖的。

如果tryAcquire()獲取失敗,則要執行addWaiter()向等待隊列中添加一個獨佔模式的節點

4.addWaiter()

private Node addWaiter(Node mode) {
    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;
        }
    }
    enq(node);
    return node;
}

這個方法的註釋:創建一個入隊node爲當前線程,Node.EXCLUSIVE 是獨佔鎖,Node.SHARED 是共享鎖。
先找到等待隊列的tail節點pred,如果pred!=null,就把當前線程添加到pred後面進入等待隊列,如果不存在tail節點執行enq()

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

這裏進行了循環,如果此時存在了tail就執行同上一步驟的添加隊尾操作,如果依然不存在,就把當前線程作爲head結點。

插入節點後,調用acquireQueued()進行阻塞

5.acquireQueued()

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

先獲取當前節點的前一節點p,如果p是head的話就再進行一次tryAcquire(arg)操作,如果成功就返回,否則就執行shouldParkAfterFailedAcquire、parkAndCheckInterrupt來達到阻塞效果;

6.shouldParkAfterFailedAcquire()

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

addWaiter()構造的新節點,waitStatus的默認值是0。此時,進入最後一個if判斷,CAS設置pred.waitStatus爲SIGNAL==-1。最後返回false。

回到第五步acquireQueued()中後,由於shouldParkAfterFailedAcquire()返回false,會繼續進行循環。假設node的前繼節點pred仍然不是頭結點或鎖獲取失敗,則會再次進入shouldParkAfterFailedAcquire()。上一輪循環中,已經將pred.waitStatus設置爲SIGNAL==-1,則這次會進入第一個判斷條件,直接返回true,表示應該阻塞。

7.parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

很顯然,一旦shouldParkAfterFailedAcquire返回true也就是應該阻塞,就會執行parkAndCheckInterrupt()來達到阻塞效果,此時線程阻塞在這裏,需要其它線程來喚醒,喚醒後就會再次循環第5步acquireQueued裏的請求邏輯。

我們回到第6步,看一個留下的邏輯片段

if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } 

什麼時候會遇到ws > 0的case呢?當pred所維護的獲取請求被取消時(也就是node的waitStatus 值爲CANCELLED),這時就會循環移除所有被取消的前繼節點pred,直到找到未被取消的pred。移除所有被取消的前繼節點後,直接返回false。

8.cancelAcquire()
到這裏我們回到第5步可以看到主體邏輯基本走完了,在該方法的finally裏有一個cancelAcquire()方法

private void cancelAcquire(Node node) {
        if (node == null)
            return;

        node.thread = null;

        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        	Node predNext = pred.next;
        	node.waitStatus = Node.CANCELLED;

        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

也就是在第5步的執行過程中,如果出現異常或者出現中斷,就會執行finally的取消線程的請求操作,核心代碼是node.waitStatus = Node.CANCELLED;將線程的狀態改爲CANCELLED。

4、釋放鎖

1.release()

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

釋放獨佔鎖,如果tryRelease成功返回true的話就會解開阻塞等待的線程
顯然,tryRelease方法來釋放鎖,如果釋放成功,先判斷head節點是否有效,最後unparkSuccessor啓動後續等待的線程。

2.tryRelease()

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

先獲取state減去釋放的一次,然後判斷當前線程是否和持有鎖線程一致,如果不一致,拋出異常,繼續判斷state的值,只有當值爲0時,free標誌才置爲true,否則說明是重入鎖,需要多次釋放直到state爲0。

3.unparkSuccessor()

private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

這個方法名:啓動後續線程,先拿到head節點的waitStatus並清空,然後獲取next節點,並做檢查,如果next節點失效,就從等待隊列的尾部進行輪詢,拿到第一個有效的節點,然後通過LockSupport.unpark(s.thread);喚醒,令該線程重新進入到獲取鎖的第5步循環去acquire鎖。

三、Synchronized與Lock的對比

1、相同點:

  1. 協調多線程對共享對象、變量的同步訪問。
  2. 可重入,同一線程可以多次獲得同一個鎖。
  3. 都保證了可見性和互斥性。
  4. 都可以形成死鎖。

2、不同點

  1. ReentrantLock 顯示獲得、釋放鎖,synchronized 隱式獲得釋放鎖。
  2. ReentrantLock 可響應中斷、可輪迴,synchronized 是不可以響應中斷的。
  3. ReentrantLock 可以手動獲得、釋放鎖,更加靈活,synchronized 自動釋放鎖,更方便、安全。
  4. ReentrantLock 是API級別的,synchronized是JVM級別的。
  5. ReentrantLock 可以實現公平鎖和非公平鎖, synchronized只能實現非公平鎖。
  6. ReentrantLock 可以拿到鎖的狀態,所以可以判斷是否成功獲得鎖,synchronized無法獲得鎖狀態。
  7. ReentrantLock 通過Condition可以綁定多個條件,可以輕鬆的解決生產者消費者問題 。
  8. ReentrantLock 發生異常時如果沒用主動釋放鎖可能會導致死鎖,synchronized 發生異常會自動釋放鎖。
  9. ReentrantLock 底層通過AQS實現,synchronized 底層通過 monitorenter 和 monitorexit 這個兩個字節碼指令實現。
  10. ReentrantLock 可以通過tryLock 方法實現限時等待獲取鎖。
  11. 待補充…
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章