深入理解顯式鎖(Lock)和AQS(AbstractQueuedSynchronizer)(超詳細)

顯式鎖

Lock

接口,實現類:ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock

有了synchronized爲什麼還要Lock?
Java程序是靠synchronized關鍵字實現鎖功能的,使用synchronized關鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放。

特性

  • 嘗試非阻塞地獲取鎖:當前線程嘗試獲取鎖,如果這一時刻沒有被其他線程獲取到,則成果獲取並持有。
  • 能被中斷的獲取鎖:與synchronized不同,獲取到鎖的線程能夠響應中斷,當獲取到鎖的線程被中斷時,中斷異常將會被拋出,同事鎖會被釋放。
  • 超時獲取鎖:在指定的截止時間之前獲取鎖,如果截止時間到了仍無法獲取鎖,則返回。

使用範式

Lock lock = ...;
lock.lock();
try{
	//業務
}finally{
	lock.unlock();
}

在finally塊中釋放鎖,目的是保證在獲取到鎖之後,最終能夠被釋放。
不要將獲取鎖的過程寫在try塊中,因爲如果在獲取鎖(自定義鎖的實現)時發生了異常,異常拋出的同時,也會導致鎖無故釋放。

API

void lock()
獲得鎖。
void lockInterruptibly()
獲取鎖定,除非當前線程是 interrupted 。
Condition newCondition()
返回一個新Condition綁定到該實例Lock實例。
boolean tryLock()
只有在調用時纔可以獲得鎖。
boolean tryLock(long time, TimeUnit unit)
如果在給定的等待時間內是空閒的,並且當前的線程尚未得到 interrupted,則獲取該鎖。
void unlock()
釋放鎖。

ReentrantLock(可重入鎖)

鎖的可重入

簡單地講就是:“同一個線程對於已經獲得到的鎖,可以多次繼續申請到該鎖的使用權”。而synchronized關鍵字隱式的支持重進入,比如一個synchronized修飾的遞歸方法,在方法執行時,執行線程在獲取了鎖之後仍能連續多次地獲得該鎖。ReentrantLock在調用lock()方法時,已經獲取到鎖的線程,能夠再次調用lock()方法獲取鎖而不被阻塞。

鎖的公平和非公平

如果在時間上,先對鎖進行獲取的請求一定先被滿足,那麼這個鎖是公平的,反之,是不公平的。公平的獲取鎖,也就是等待時間最長的線程最優先獲取鎖,也可以說鎖獲取是順序的。 ReentrantLock提供了一個構造函數,能夠控制鎖是否是公平的。事實上,公平的鎖機制往往沒有非公平的效率高。
在激烈競爭的情況下,非公平鎖的性能高於公平鎖的性能的一個原因是:在恢復一個被掛起的線程與該線程真正開始運行之間存在着嚴重的延遲。假設線程A持有一個鎖,並且線程B請求這個鎖。由於這個鎖已被線程A持有,因此B將被掛起。當A釋放鎖時,B將被喚醒,因此會再次嘗試獲取鎖。與此同時,如果C也請求這個鎖,那麼C很可能會在B被完全喚醒之前獲得、使用以及釋放這個鎖。這樣的情況是一種“雙贏”的局面:B獲得鎖的時刻並沒有推遲,C更早地獲得了鎖,並且吞吐量也獲得了提高。

API

構造方法:
ReentrantLock()
創建一個 ReentrantLock的實例。
ReentrantLock(boolean fair)
根據給定的公平政策創建一個 ReentrantLock的實例。

其他方法:
int getHoldCount()
查詢當前線程對此鎖的暫停數量。
protected Thread getOwner()
返回當前擁有此鎖的線程,如果不擁有,則返回 null 。
protected Collection< Thread > getQueuedThreads()
返回包含可能正在等待獲取此鎖的線程的集合。
int getQueueLength()
返回等待獲取此鎖的線程數的估計。
protected Collection< Thread > getWaitingThreads(Condition condition)
返回包含可能在與此鎖相關聯的給定條件下等待的線程的集合。
int getWaitQueueLength(Condition condition)
返回與此鎖相關聯的給定條件等待的線程數的估計。
boolean hasQueuedThread(Thread thread)
查詢給定線程是否等待獲取此鎖。
boolean hasQueuedThreads()
查詢是否有線程正在等待獲取此鎖。
boolean hasWaiters(Condition condition)
查詢任何線程是否等待與此鎖相關聯的給定條件。
boolean isFair()
如果此鎖的公平設置爲true,則返回 true 。
boolean isHeldByCurrentThread()
查詢此鎖是否由當前線程持有。
boolean isLocked()
查詢此鎖是否由任何線程持有。
void lock()
獲得鎖。
void lockInterruptibly()
獲取鎖定,除非當前線程是 interrupted 。
Condition newCondition()
返回Condition用於這種用途實例Lock實例。
String toString()
返回一個標識此鎖的字符串以及其鎖定狀態。
boolean tryLock()
只有在調用時它不被另一個線程佔用才能獲取鎖。
boolean tryLock(long timeout, TimeUnit unit)
如果在給定的等待時間內沒有被另一個線程 佔用 ,並且當前線程尚未被 保留,則獲取該鎖( interrupted) 。
void unlock()
嘗試釋放此鎖。

Condition

接口,實現類:AbstractQueuedLongSynchronizer.ConditionObject , AbstractQueuedSynchronizer.ConditionObject
任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式。

使用範式

 Lock lock = new ReentrantLock();
 Condition condition = lock.newCondition();
 public void conditionWait{
		lock.lock();
    	try {
    		condition.await();
    		//業務
    	}finally {
			lock.unlock();
    	}      
    }
 public void conditionSignal{
		lock.lock();
    	try {
    		//業務
    		condition.signal();
    	}finally {
			lock.unlock();
    	}      
    }

示例:

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

/**
 *類說明:快遞實體類
 */
public class ExpressCond {
    public final static String CITY = "ShangHai";
    private int km;/*快遞運輸里程數*/
    private String site;/*快遞到達地點*/
    private Lock kmLock = new ReentrantLock();
    private Lock siteLock = new ReentrantLock();
    private Condition kmCond = kmLock.newCondition();
    private Condition siteCond = siteLock.newCondition();


    public ExpressCond(int km, String site) {
        this.km = km;
        this.site = site;
    }

    /* 變化公里數,然後通知處於wait狀態並需要處理公里數的線程進行業務處理*/
    public void changeKm(){
        kmLock.lock();
        try{
            this.km = 101;
            kmCond.signal();
        }finally {
            kmLock.unlock();
        }
        
        
    }

    /* 變化地點,然後通知處於wait狀態並需要處理地點的線程進行業務處理*/
    public  void changeSite(){
    	siteLock.lock();
    	try {
    		this.site = "BeiJing";
    		siteCond.signal();//通知其他在鎖上等待的線程
    	}finally {
    		siteLock.unlock();
    	}
    }

    /*當快遞的里程數大於100時更新數據庫*/
    public void waitKm(){
        kmLock.lock();
        try{
            while(this.km<100){
                try {
                    kmCond.await();
                    System.out.println("Check Site thread["
                            +Thread.currentThread().getId()
                            +"] is be notified");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {
            kmLock.unlock();
        }


        System.out.println("the Km is "+this.km+",I will change db");
    }

    /*當快遞到達目的地時通知用戶*/
    public void waitSite(){
    	siteLock.lock();
    	try {
        	while(this.site.equals(CITY)) {
        		try {
    				siteCond.await();//當前線程進行等待
    				System.out.println("check Site thread["+Thread.currentThread().getName()
    						+"] is be notify");
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
        	}
    	}finally {
    		siteLock.unlock();
    	}

        System.out.println("the site is "+this.site+",I will call user");
    }
}

/**
 *類說明:測試Lock和Condition實現等待通知
 */
public class TestCond {
    private static ExpressCond express = new ExpressCond(0,ExpressCond.CITY);

    /*檢查里程數變化的線程,不滿足條件,線程一直等待*/
    private static class CheckKm extends Thread{
        @Override
        public void run() {
        	express.waitKm();
        }
    }

    /*檢查地點變化的線程,不滿足條件,線程一直等待*/
    private static class CheckSite extends Thread{
        @Override
        public void run() {
        	express.waitSite();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<3;i++){
            new CheckSite().start();
        }
        for(int i=0;i<3;i++){
            new CheckKm().start();
        }

        Thread.sleep(1000);
        express.changeKm();

        Thread.sleep(5000);
        System.out.println("***********************");
        express.changeSite();

    }
}

結果:
在這裏插入圖片描述
可以與第一章用wait()和notifyAll()實現的示例比較,思考爲什麼結果不一樣。

API

void await()
導致當前線程等到發信號或 interrupted 。
boolean await(long time, TimeUnit unit)
使當前線程等待直到發出信號或中斷,或指定的等待時間過去。
long awaitNanos(long nanosTimeout)
使當前線程等待直到發出信號或中斷,或指定的等待時間過去。
void awaitUninterruptibly()
使當前線程等待直到發出信號。
boolean awaitUntil(Date deadline)
使當前線程等待直到發出信號或中斷,或者指定的最後期限過去。
void signal()
喚醒一個等待線程。
void signalAll()
喚醒所有等待線程。

LockSupport

LockSupport定義了一組的公共靜態方法,這些方法提供了最基本的線程阻塞和喚醒功能,而LockSupport也成爲構建同步組件的基礎工具。
LockSupport定義了一組以park開頭的方法用來阻塞當前線程,以及unpark(Thread thread)方法來喚醒一個被阻塞的線程。LockSupport增加了park(Object blocker)、parkNanos(Object blocker,long nanos)和parkUntil(Object blocker,long deadline)3個方法,用於實現阻塞當前線程的功能,其中參數blocker是用來標識當前線程在等待的對象(以下稱爲阻塞對象),該對象主要用於問題排查和系統監控。

CLH(隊列鎖)

Java中的AQS是CLH隊列鎖的一種變體實現,要學AQS,先了解CLH。

CLH隊列鎖即Craig, Landin, and Hagersten locks。三個人的名字。
CLH隊列鎖也是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程僅僅在本地變量上自旋,它不斷輪詢前驅的狀態,假設發現前驅釋放了鎖就結束自旋。

原理

當一個線程需要獲取鎖時:

  1. 創建一個的QNode,將其中的locked設置爲true表示需要獲取鎖,myPred表示對其前驅結點的引用
    在這裏插入圖片描述
  2. 線程A對tail域調用getAndSet方法,使自己成爲隊列的尾部,同時獲取一個指向其前驅結點的引用myPred
    在這裏插入圖片描述
    線程B需要獲得鎖,同樣的流程再來一遍
    在這裏插入圖片描述
  3. 線程就在前驅結點的locked字段上旋轉,直到前驅結點釋放鎖(前驅節點的鎖值 locked == false)
  4. 當一個線程需要釋放鎖時,將當前結點的locked域設置爲false,同時回收前驅結點
    在這裏插入圖片描述

如上圖所示,前驅結點釋放鎖,線程A的myPred所指向的前驅結點的locked字段變爲false,線程A就可以獲取到鎖。
CLH隊列鎖的優點是空間複雜度低(如果有n個線程,L個鎖,每個線程每次只獲取一個鎖,那麼需要的存儲空間是O(L+n),n個線程有n個myNode,L個鎖有L個tail)。CLH隊列鎖常用在SMP體系結構下。

擴展

SMP(Symmetric Multi-Processor),即對稱多處理器結構,指server中多個CPU對稱工作,每一個CPU訪問內存地址所需時間同樣。其主要特徵是共享,包括對CPU,內存,I/O等進行共享。SMP的長處是可以保證內存一致性。缺點是這些共享的資源非常可能成爲性能瓶頸。隨着CPU數量的添加,每一個CPU都要訪問同樣的內存資源,可能導致內存訪問衝突,可能會導致CPU資源的浪費。經常使用的PC機就屬於這樣的。

非一致存儲訪問,將CPU分爲CPU模塊,每個CPU模塊由多個CPU組成,並且具有獨立的本地內存、I/O槽口等,模塊之間可以通過互聯模塊相互訪問,訪問本地內存(本CPU模塊的內存)的速度將遠遠高於訪問遠地內存(其他CPU模塊的內存)的速度,這也是非一致存儲訪問的由來。NUMA(Non Uniform Memory Access) 較好地解決SMP的擴展問題,當CPU數量增加時,因爲訪問遠地內存的延時遠遠超過本地內存,系統性能無法線性增加。

CLH唯一的缺點是在NUMA系統結構下性能很差,但是在SMP系統結構下該法還是非常有效的。解決NUMA系統結構的思路是MCS隊列鎖。

AQS(隊列同步器)

學習AQS的必要性

隊列同步器AbstractQueuedSynchronizer(以下簡稱同步器或AQS),是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。併發包的大師(Doug Lea)期望它能夠成爲實現大部分同步需求的基礎。

AQS使用方式和其中的設計模式

使用方式

AQS的主要使用方式是繼承,子類通過繼承AQS並實現它的抽象方法來管理同步狀態,在AQS裏由一個int型的state來代表這個狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的3個方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))來進行操作,因爲它們能夠保證狀態的改變是安全的。
在這裏插入圖片描述
在實現上,子類推薦被定義爲自定義同步組件的靜態內部類,AQS自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既可以支持獨佔式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型的同步組件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
同步器是實現鎖(也可以是任意同步組件)的關鍵,在鎖的實現中聚合同步器。可以這樣理解二者之間的關係:
鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個線程並行訪問),隱藏了實現細節;
同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域。
實現者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。

模板方法模式

同步器的設計基於模板方法模式。模板方法模式的意圖是,定義一個操作中的算法的骨架,而將一些步驟的實現延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。我們最常見的就是Spring框架裏的各種Template。

點擊這裏瞭解模板方法模式。

AQS中的方法

模板方法

實現自定義同步組件時,將會調用同步器提供的模板方法

void acquire(int arg)
獨佔式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回。否則,將會進入同步隊列等待,該方法會調用重寫的tryAcquire(int arg)方法。
void acquireInterruptibly(int arg)
與acquire(int arg) 相比,該方法響應中斷。當前線程未獲取到同步狀態面進入同步隊列中,如果當前線程被中斷,則該方法會拋出InterruptedException並返回。
void acquireShared(int arg)
共享式獲取同步狀態,如果當前線程未獲取同步狀態,將會進入同步隊列等待。與獨佔式獲取的主要區別是在同一時刻可以有多個線程獲取到同步狀態。
void acquireSharedInterruptibly(int arg)
與acquireShared(int arg)相比,該方法響應中斷。
boolean tryAcquireNanos(int arg, long nanosTimeout)
與tryAcquire(int arg)相比增加了超時限制。
boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
與tryAcquireShared(int arg)相比增加了超時限制。
boolean release(int arg)
獨佔式釋放同步狀態。該方法會在釋放同步狀態之後,將同步隊列中第一個節點包含的線程喚醒。
boolean releaseShared(int arg)
共享式釋放同步狀態。
Collection< Thread > getQueuedThreads()
返回一個包含可能正在等待獲取的線程的集合。 因爲在構建此結果時,實際的線程集可能會動態更改,所以返回的集合只是盡力而爲的估計。 返回的集合的元素沒有特定的順序。 該方法旨在便於構建提供更廣泛監控設施的子類。

可以重寫的方法

protected boolean tryAcquire(int arg)
獨佔式獲取同步狀態,實現該方法需要查詢當前狀態並判斷同步狀態是否符合預期,然後再進行CAS設置同步狀態。
protected int tryAcquireShared(int arg)
共享式獲取同步狀態,返回 大於等於0的值,表示獲取成功,反之,獲取失敗。
protected boolean tryRelease(int arg)
獨佔式釋放同步狀態。等待獲取同步狀態的線程將有機會獲取同步狀態。
protected boolean tryReleaseShared(int arg)
共享式釋放同步狀態。
protected boolean isHeldExclusively()
當前同步器是否在獨享模式下被線程佔用,一般該方法表示是否被當前線程獨佔。

訪問或修改同步狀態的方法

重寫同步器指定的方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態。
protected int getState()
獲取當前同步狀態。
protected void setState(int newState)
設置當前同步狀態。
protected boolean compareAndSetState(int expect, int update)
使用CAS設置當前狀態,該方法能夠保證狀態設置的原子性。

AQS中的節點(Node)

既然說Java中的AQS是CLH隊列鎖的一種變體實現,毫無疑問,作爲隊列來說,必然要有一個節點的數據結構來保存我們前面所說的各種域,比如前驅節點,節點的狀態等,這個數據結構就是AQS中的內部類Node。作爲這個數據結構應該關心些什麼信息?
1、線程信息,肯定要知道我是哪個線程;
2、隊列中線程狀態,既然知道是哪一個線程,肯定還要知道線程當前處在什麼狀態,是已經取消了“獲鎖”請求,還是在“”等待中”,或者說“即將得到鎖”
3、前驅和後繼線程,因爲是一個等待隊列,那麼也就需要知道當前線程前面的是哪個線程,當前線程後面的是哪個線程(因爲當前線程釋放鎖以後,理當立馬通知後繼線程去獲取鎖)。
所以這個Node類是這麼設計的:
在這裏插入圖片描述

線程的2種等待模式
SHARED:表示線程以共享的模式等待鎖(如ReadLock)
EXCLUSIVE:表示線程以互斥的模式等待鎖(如ReetrantLock),互斥就是一把鎖只能由一個線程持有,不能同時存在多個線程使用同一個鎖。

線程在隊列中的狀態枚舉
CANCELLED:值爲1,表示線程的獲鎖請求已經“取消”
SIGNAL:值爲-1,表示該線程一切都準備好了,就等待鎖空閒出來給我
CONDITION:值爲-2,表示線程等待某一個條件(Condition)被滿足
PROPAGATE:值爲-3,當線程處在“SHARED”模式時,該字段纔會被使用上,釋放共享資源時需要通知其他節點
初始化Node對象時,默認爲0

成員變量
waitStatus:該int變量表示線程在隊列中的狀態,其值就是上述提到的CANCELLED、SIGNAL、CONDITION、PROPAGATE
prev:該變量類型爲Node對象,表示該節點的前一個Node節點(前驅)
next:該變量類型爲Node對象,表示該節點的後一個Node節點(後繼)
thread:該變量類型爲Thread對象,表示該節點的代表的線程
nextWaiter:該變量類型爲Node對象,表示等待condition條件的Node節點
當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成爲一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。同步隊列中的節點(Node)用來保存獲取同步狀態失敗的線程引用、等待狀態以及前驅和後繼節點。

head和tail
AQS還擁有首節點(head)和尾節點(tail)兩個引用,一個指向隊列頭節點,而另一個指向隊列尾節點。
注意:因爲首節點head是不保存線程信息的節點,僅僅是因爲數據結構設計上的需要,在數據結構上,這種做法往往叫做“空頭節點鏈表”。對應的就有“非空頭結點鏈表”。

入隊和出隊

節點加入到同步隊列
當一個線程成功地獲取了同步狀態(或者鎖),其他線程將無法獲取到同步狀態,也就是獲取同步狀態失敗,AQS會將這個線程以及等待狀態等信息構造成爲一個節點(Node)並將其加入同步隊列的尾部。而這個加入隊列的過程必須要保證線程安全,因此同步器提供了一個基於CAS的設置尾節點的方法:compareAndSetTail(Node expect,Node update),它需要傳遞當前線程“認爲”的尾節點和當前節點,只有設置成功後,當前節點才正式與之前的尾節點建立關聯。
在這裏插入圖片描述

首節點的變化
首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點。設置首節點是通過獲取同步狀態成功的線程來完成的,由於只有一個線程能夠成功獲取到同步狀態,因此設置頭節點的方法並不需要使用CAS來保證,它只需要將首節點設置成爲原首節點的後繼節點並斷開原首節點的next引用即可。
在這裏插入圖片描述

獨佔式同步狀態獲取與釋放

在這裏插入圖片描述

獲取

通過調用同步器的acquire(int arg)方法可以獲取同步狀態,主要完成了同步狀態獲取、節點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作,其主要邏輯是:

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

首先調用自定義同步器實現的tryAcquire(int arg)方法,該方法需要保證線程安全的獲取同步狀態。
如果同步狀態獲取失敗(tryAcquire返回false),則構造同步節點(獨佔式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態)並通過addWaiter(Node node)方法將該節點加入到同步隊列的尾部,
最後調用acquireQueued(Node node,int arg)方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。
addWaiter(Node node)方法中

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

將當前線程包裝成Node後,隊列不爲空的情況下,先嚐試把當前節點加入隊列併成爲尾節點,如果不成功或者隊列爲空進入enq(final Node node)方法。

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;
                }
            }
        }
    }

在enq(final Node node)方法中,同步器通過“死循環”來保證節點的正確添加,這個死循環中,做了兩件事,第一件,如果隊列爲空,初始化隊列,new出一個空節點,並讓首節點(head)和尾節點(tail)兩個引用都指向這個空節點;第二件事,把當前節點加入隊列。
在“死循環”中只有通過CAS將節點設置成爲尾節點之後,當前線程才能從該方法返回,否則,當前線程不斷地嘗試設置。
節點進入同步隊列之後,觀察acquireQueued(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();
                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);
        }
    }

其實就是一個自旋的過程,每個節點(或者說每個線程)都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出;否則會阻塞節點的線程,直到下一次其它線程釋放同步狀態會喚醒此線程,繼續自旋。
在acquireQueued(final Node node,int arg)方法中,當前線程在“死循環”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能夠嘗試獲取同步狀態,這是爲什麼?原因有兩個。
第一,頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態之後,將會喚醒其後繼節點,後繼節點的線程被喚醒後需要檢查自己的前驅節點是否是頭節點。
第二,維護同步隊列的FIFO原則。
當前線程獲取到同步狀態後,讓首節點(head)這個引用指向自己所在節點。當同步狀態獲取成功後,當前線程就從acquire方法返回了。如果同步器實現的是鎖,那就代表當前線程獲得了鎖。

釋放

當前線程獲取同步狀態並執行了相應邏輯之後,就需要釋放同步狀態,使得後續節點能夠繼續獲取同步狀態。通過調用同步器的release(int arg)方法可以釋放同步狀態,該方法在釋放了同步狀態之後,會喚醒其後繼節點(進而使後繼節點重新嘗試獲取同步狀態)。

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

該方法執行時,會喚醒首節點(head)所指向節點的後繼節點線程,unparkSuccessor(Node node)方法使用LockSupport來喚醒處於等待狀態的線程。
而在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指向節點的後繼節點線程,如果這個後繼節點處於被cancel狀態,(開發者的思路可能是這樣的:後繼節點處於被cancel狀態,意味着當鎖競爭激烈時,隊列的第一個節點等了很久(一直被還未加入隊列的節點搶走鎖),包括後續的節點cancel的機率都比較大,所以)先從尾開始遍歷,找到最前面且沒有被cancel的節點。

總結

在獲取同步狀態時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中並在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節點爲頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然後喚醒head指向節點的後繼節點。

共享式同步狀態獲取與釋放

共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態。以讀寫爲例,如果一個程序在進行讀操作,那麼這一時刻寫操作均被阻塞,而讀操作能夠同時進行。寫操作要求對資源的獨佔式訪問,而讀操作可以是共享式訪問。

 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

在acquireShared(int arg)方法中,同步器調用tryAcquireShared(int arg)方法嘗試獲取同步狀態,tryAcquireShared(int arg)方法返回值爲int類型,當返回值大於等於0時,表示能夠獲取到同步狀態。因此,在共享式獲取的自旋過程中,成功獲取到同步狀態並退出自旋的條件就是tryAcquireShared(int arg)方法返回值大於等於0。

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

可以看到,在doAcquireShared(int arg)方法的自旋過程中,如果當前節點的前驅爲頭節點時,嘗試獲取同步狀態,如果返回值大於等於0,表示該次獲取同步狀態成功,將此節點設置爲頭結點並傳播。

該方法在釋放同步狀態之後,將會喚醒後續處於等待狀態的節點。對於能夠支持多個線程同時訪問的併發組件(比如Semaphore),它和獨佔式主要區別在於tryReleaseShared(int arg)方法必須確保同步狀態(或者資源數)線程安全釋放,一般是通過循環和CAS來保證的,因爲釋放同步狀態的操作會同時來自多個線程。

 private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
     
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

Condition的實現

等待隊列是一個FIFO的隊列,在隊列中的每個節點都包含了一個線程引用,該線程就是在Condition對象上等待的線程,如果一個線程調用了Condition.await()方法,那麼該線程將會釋放鎖、構造成節點加入等待隊列並進入等待狀態。事實上,節點的定義複用了同步器中節點的定義,也就是說,同步隊列和等待隊列中節點類型都是同步器的靜態內部類。
在這裏插入圖片描述
一個Condition包含一個等待隊列,Condition擁有首節點(firstWaiter)和尾節點(lastWaiter)。當前線程調用Condition.await()方法,將會以當前線程構造節點,並將節點從尾部加入等待隊列。Condition擁有首尾節點的引用,而新增節點只需要將原有的尾節點nextWaiter指向它,並且更新尾節點即可。上述節點引用更新的過程並沒有使用CAS保證,原因在於調用await()方法的線程必定是獲取了鎖的線程,也就是說該過程是由鎖來保證線程安全的。
Lock(更確切地說是同步器)擁有一個同步隊列和多個等待隊列。
在這裏插入圖片描述
調用Condition的await()方法(或者以await開頭的方法),會使當前線程進入等待隊列並釋放鎖,同時線程狀態變爲等待狀態。當從await()方法返回時,當前線程一定獲取了Condition相關聯的鎖。
如果從隊列(同步隊列和等待隊列)的角度看await()方法,當調用await()方法時,相當於同步隊列的首節點(獲取了鎖的節點)移動到Condition的等待隊列中。調用該方法的線程成功獲取了鎖的線程,也就是同步隊列中的首節點,該方法會將當前線程構造成節點並加入等待隊列中,然後釋放同步狀態,喚醒同步隊列中的後繼節點,然後當前線程會進入等待狀態。當等待隊列中的節點被喚醒,則喚醒節點的線程開始嘗試獲取同步狀態。如果不是通過其他線程調用Condition.signal()方法喚醒,而是對等待線程進行中斷,則會拋出InterruptedException。
在這裏插入圖片描述
如圖所示,同步隊列的首節點並不會直接加入等待隊列,而是通過addConditionWaiter()方法把當前線程構造成一個新的節點並將其加入等待隊列中。
在這裏插入圖片描述
調用Condition的signal()方法,會將在等待隊列中等待時間最長的節點(首節點)移到同步隊列尾部,並喚醒。這時節點是開始自旋,依舊要在同步隊列中排隊,而不是直接獲取同步狀態。
調用該方法的前置條件是當前線程必須獲取了鎖,可以看到signal()方法進行了isHeldExclusively()檢查,也就是當前線程必須是獲取了鎖的線程。接着獲取等待隊列的首節點,將其移動到同步隊列並使用LockSupport喚醒節點中的線程。
通過調用同步器的enq(Node node)方法,等待隊列中的頭節點線程安全地移動到同步隊列。當節點移動到同步隊列後,當前線程再使用LockSupport喚醒該節點的線程。
被喚醒後的線程,將從await()方法中的while循環中退出(isOnSyncQueue(Node node)方法返回true,節點已經在同步隊列中),進而調用同步器的acquireQueued()方法加入到獲取同步狀態的競爭中。
成功獲取同步狀態(或者說鎖)之後,被喚醒的線程將從先前調用的await()方法返回,此時該線程已經成功地獲取了鎖。
Condition的signalAll()方法,相當於對等待隊列中的每個節點均執行一次signal()方法,效果就是將等待隊列中所有節點全部移動到同步隊列中,並喚醒每個節點的線程。

Lock的實現

ReentrantLock的實現

鎖的可重入

重進入是指任意線程在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞,該特性的實現需要解決以下兩個問題。
1)線程再次獲取鎖。鎖需要去識別獲取鎖的線程是否爲當前佔據鎖的線程,如果是,則再次成功獲取。
2)鎖的最終釋放。線程重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他線程能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放。
nonfairTryAcquire方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前線程是否爲獲取鎖的線程來決定獲取操作是否成功,如果是獲取鎖的線程再次請求,則將同步狀態值進行增加並返回true,表示獲取同步狀態成功。同步狀態表示鎖被一個線程重複獲取的次數。
如果該鎖被獲取了n次,那麼前(n-1)次tryRelease(int releases)方法必須返回false,而只有同步狀態完全釋放了,才能返回true。可以看到,該方法將同步狀態是否爲0作爲最終釋放的條件,當同步狀態爲0時,將佔有線程設置爲null,並返回true,表示釋放成功。

公平和非公平鎖

ReentrantLock的構造函數中,默認的無參構造函數將會把Sync對象創建爲NonfairSync對象,這是一個“非公平鎖”;而另一個構造函數ReentrantLock(boolean fair)傳入參數爲true時將會把Sync對象創建爲“公平鎖”FairSync。
nonfairTryAcquire(int acquires)方法,對於非公平鎖,只要CAS設置同步狀態成功,則表示當前線程獲取了鎖,而公平鎖則不同。tryAcquire方法,該方法與nonfairTryAcquire(int acquires)比較,唯一不同的位置爲判斷條件多了hasQueuedPredecessors()方法,即加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之後才能繼續獲取鎖。
改造我們的獨佔鎖爲可重入

ReentrantReadWriteLock的實現

讀寫狀態的設計

讀寫鎖同樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。
回想ReentrantLock中自定義同步器的實現,同步狀態表示鎖被一個線程重複獲取的次數,而讀寫鎖的自定義同步器需要在同步狀態(一個整型變量)上維護多個讀線程和一個寫線程的狀態,使得該狀態的設計成爲讀寫鎖實現的關鍵。
如果在一個整型變量上維護多種狀態,就一定需要“按位切割使用”這個變量,讀寫鎖將變量切分成了兩個部分,高16位表示讀,低16位表示寫,讀寫鎖是如何迅速確定讀和寫各自的狀態呢?
答案是通過位運算。假設當前同步狀態值爲S,寫狀態等於S&0x0000FFFF(將高16位全部抹去),讀狀態等於S>>>16(無符號補0右移16位)。當寫狀態增加1時,等於S+1,當讀狀態增加1時,等於S+(1<<16),也就是S+0x00010000。根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S&0x0000FFFF)等於0時,則讀狀態(S>>>16)大於0,即讀鎖已被獲取。

寫鎖的獲取與釋放

寫鎖是一個支持重進入的排它鎖。如果當前線程已經獲取了寫鎖,則增加寫狀態。如果當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不爲0)或者該線程不是已經獲取寫鎖的線程,則當前線程進入等待狀態。
該方法除了重入條件(當前線程爲獲取了寫鎖的線程)之外,增加了一個讀鎖是否存在的判斷。如果存在讀鎖,則寫鎖不能被獲取,原因在於:讀寫鎖要確保寫鎖的操作對讀鎖可見,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那麼正在運行的其他讀線程就無法感知到當前寫線程的操作。因此,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而寫鎖一旦被獲取,則其他讀寫線程的後續訪問均被阻塞。
寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態爲0時表示寫鎖已被釋放,從而等待的讀寫線程能夠繼續訪問讀寫鎖,同時前次寫線程的修改對後續讀寫線程可見。
讀鎖的獲取與釋放
讀鎖是一個支持重進入的共享鎖,它能夠被多個線程同時獲取,在沒有其他寫線程訪問(或者寫狀態爲0)時,讀鎖總會被成功地獲取,而所做的也只是(線程安全的)增加讀狀態。如果當前線程已經獲取了讀鎖,則增加讀狀態。
如果當前線程在獲取讀鎖時,寫鎖已被其他線程獲取,則進入等待狀態。讀狀態是所有線程獲取讀鎖次數的總和,而每個線程各自獲取讀鎖的次數只能選擇保存在ThreadLocal中,由線程自身維護。在tryAcquireShared(int unused)方法中,如果其他線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。如果當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增加讀狀態,成功獲取讀鎖。讀鎖的每次釋放(線程安全的,可能有多個讀線程同時釋放讀鎖)均減少讀狀態。

鎖的升降級

鎖降級指的是寫鎖降級成爲讀鎖。如果當前線程擁有寫鎖,然後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之爲鎖降級。
鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。
RentrantReadWriteLock不支持鎖升級(把持讀鎖、獲取寫鎖,最後釋放讀鎖的過程)。目的是保證數據可見性,如果讀鎖已被多個線程獲取,其中任意線程成功獲取了寫鎖並更新了數據,則其更新對其他獲取到讀鎖的線程是不可見的。

參考:Mark—筆記_Java併發編程

【併發編程】目錄:

【併發編程】之走進Java裏的線程世界

【併發編程】之學會使用線程的併發工具類

【併發編程】之學會使用原子操作CAS

【併發編程】之深入理解顯式鎖和AQS

【併發編程】之一文徹底搞懂併發容器

【併發編程】之Java面試經常會問到的線程池,你搞清楚了嗎?

【併發編程】之Java併發安全知識點總結

【併發編程】之大廠很可能會問到的JMM底層實現原理

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