1.Lock 接口
??鎖是用來控制多個線程訪問共享資源的方式,一般來說, 一個鎖能夠防止多個線程同時訪問共享資源(但是有些鎖可以允許多個線程并發(fā)的訪問共享資源, 比如讀寫鎖)。在Lock接口出現(xiàn)之前,Java程序是靠synchronized關(guān)鍵字實現(xiàn)鎖功能的, 而Java SE 5之后,并發(fā)包中新增了Lock接口(以及相關(guān)實現(xiàn)類)用來實現(xiàn)鎖功能,它提供了與synchronized關(guān)鍵字類似的同步功能, 只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了(通過synchronized塊或者方法所提供的)隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、 可中斷的獲取鎖以及超時獲取鎖等多種synchronized關(guān)鍵字所不具備的同步特性。
??使用synchronized關(guān)鍵字將會隱式地獲取鎖,但是 它將鎖的獲取和釋放固化了,也就是先獲取再釋放。當然,這種方式簡化了同步的管理,可是擴展性沒有顯示的鎖獲取和釋放來的好。
??不要將獲取鎖的過程寫在try塊中, 因為如果在獲取鎖(自定義鎖的實現(xiàn))時發(fā)生了異常, 異常拋出的同時, 也會導(dǎo)致鎖無故釋放。
??Lock 接口提供的 synchronized 關(guān)鍵字所不具備的主要特性如下表所示。

??Lock 是一個接口,它定義了鎖獲取和釋放的基本操作,Lock的API如下表:

??Lock接口的實現(xiàn)基本都是能過聚合了一個同步器的子類完成線程訪問控制的。
2.隊列同步器
??隊列同步器AbstractQueuedSynchronizer (以下簡稱同步器), 是用來構(gòu)建鎖或者其他同步組件的基礎(chǔ)框架, 它使用了一個int成員變量表示同步狀態(tài), 通過內(nèi)置的FIFO隊列來完成資源獲取線程的排隊工作, 并發(fā)包的作者(Doug Lea )期望它能夠成為實現(xiàn)大部分同步需求 的基礎(chǔ)。
??同步器的主要使用方式是繼承, 子類通過繼承同步器并實現(xiàn)它的抽象方法來管理同步狀態(tài), 在抽象方法的實現(xiàn)過程中免不了要對同步狀態(tài)進行更改, 這時就需要使用同步器提供 的3個 方法(getState()、setState(int newState)和compareAndSetState(int expect, int update)) 來進行操作,因為它們能夠保證狀態(tài)的改變是安全的。 子類推薦被定義為自定義同步組件 的靜態(tài)內(nèi)部類, 同步器自身沒有實現(xiàn)任何同步接口, 它僅僅是定義了若干同步狀態(tài)獲取和釋放的方法來供自定義同步組件使用, 同步器既可以支持獨占式地獲取同步狀態(tài), 也可以支持共享式地獲取同步狀態(tài), 這樣就可以方便實現(xiàn)不同類型的同步組件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等) 。
??同步器是實現(xiàn)鎖(也可以是任意同步組件) 的關(guān)鍵, 在鎖的實現(xiàn)中聚合同步器, 利用同步器實現(xiàn)鎖的語義。 可以這樣理解二者之間的關(guān)系:鎖是面向使用者的, 它定義了使用者與 鎖交互的接口(比如可以允許兩個線程并行訪問), 隱藏了實現(xiàn)細節(jié);同步器面向的是鎖的 實現(xiàn)者, 它簡化了鎖的實現(xiàn)方式, 屏蔽了同步狀態(tài)管理、 線程的排隊、等待與喚醒等底層操作。 鎖和同步器很好地隔離了使用者和實現(xiàn)者所需關(guān)注的領(lǐng)域。
2.1 隊列同步器的接口與示例
??同步器的設(shè)計是基于模板方法模式的,使用者需要繼承同步器并重寫指定的方法,隨后將同步器組合在自定義同步組件的實現(xiàn)中,并調(diào)用同步器提供的模板方法,而這些模板方法將會調(diào)用使用者重寫的方法。
??重寫同步器指定的方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態(tài)。
??- getState(); 獲取當前同步狀態(tài)。
??- setState(int newState):設(shè)置當前同步狀態(tài)。
??- compareAndSetState(int expect, int update):使用CAS設(shè)置當前狀態(tài),該方法能夠保證狀態(tài)設(shè)置的原子性。


??實現(xiàn)自定義同步組件時,將會調(diào)用同步器提供的模板方法,這些(部分)模板方法與描述如下:

??同步器提供的模板方法基本上分為3類:獨占式獲取與釋放同步狀態(tài)、 共享式獲取與釋放同步狀態(tài)和查詢同步隊列中的等待線程情況。 自定義同步組件將使用同步器提供的模板方法來實現(xiàn)自己的同步語義。
??只有掌握了同步器的工作原理才能更加深入地理解并發(fā)包中其他的并發(fā)組件, 所以下面通過一個獨占鎖的示例來深入了解一下同步器的工作原理。
??顧名思義, 獨占鎖就是在同一時刻只能有一個線程獲取到鎖, 而其他獲取鎖的線程只能處于同步隊列中等待, 只有獲取鎖的線程釋放了鎖, 后繼的線程才能夠獲取鎖。
2.2 隊列同步器的實現(xiàn)分析
??從實現(xiàn)角度分析同步器是如何完成線程同步的,主要包括:同步隊列、獨占式 同步狀態(tài)獲取與釋放、共享式同步狀態(tài)獲取與釋放以及超時獲取同步狀態(tài)等同步器的核心數(shù)據(jù)結(jié)構(gòu)與模板方法。
??1.同步隊列
??同步器依賴內(nèi)部的同步隊列(一個FIFO雙向隊列)來完成同步狀態(tài)的管理,當前線程獲取同步狀態(tài)失敗時,同步器會將當前線程以及等待狀態(tài)等信息構(gòu)造成為一個節(jié)點(Node) 并將其加入同步隊列,同時會阻塞當前線程,當同步狀態(tài)釋放時,會把首節(jié)點中的線程喚醒,使其再次嘗試獲取同步狀態(tài)。
??同步隊列中的節(jié)點(Node)用來保存獲取同步狀態(tài)失敗的線程引用、等待狀態(tài)以及前驅(qū)和后繼節(jié)點,節(jié)點的屬性類型與名稱以及描述如下表所示。

??節(jié)點是構(gòu)成同步隊列(等待隊列) 的基礎(chǔ),同步器擁有首節(jié)點 ( head)和尾節(jié)點(tail),沒有成功獲取同步狀態(tài)的線程將會成為節(jié)點加入該隊列的尾部,同步隊列的基本結(jié)構(gòu)如下圖所示。

??同步器包含了兩個節(jié)點類型的引用, 一個指向頭節(jié)點, 而另一個指向尾節(jié)點。 試想一下, 當一個線程成功地獲取了同步狀態(tài)(或者鎖), 其他線程將無法獲取到同步狀態(tài), 轉(zhuǎn)而被構(gòu)造成為節(jié)點并加入到同步隊列中, 而這個加入隊列的過程必須要保證線程安全,因此同步器提供了一個基于CAS的設(shè)置尾節(jié)點的方法:compareAndSetTail(Node expect, Node update), 它需要傳遞當前線程 “認為” 的尾節(jié)點和當前節(jié)點, 只有設(shè)置成功后, 當前節(jié)點才正式與之前的尾節(jié)點建立關(guān)聯(lián)。
??同步器將節(jié)點加入到同步隊列的過程如圖所示。

??同步隊列遵循FIFO, 首節(jié)點是獲取同步狀態(tài)成功的節(jié)點, 首節(jié)點的線程在釋放同步狀態(tài)時, 將會喚醒后繼節(jié)點, 而后繼節(jié)點將會在獲取同步狀態(tài)成功時將自己設(shè)置為首節(jié)點, 該過程如圖所示。

??設(shè)置首節(jié)點是通過獲取同步狀態(tài)成功的線程來完成的, 由于只有一個線程能夠成功獲取到同步狀態(tài), 因此設(shè)置頭節(jié)點的方法并不需要使用CAS來保證, 它只需要將首節(jié)點設(shè)置成為原首節(jié)點的后繼節(jié)點并斷開原首節(jié)點的 next引用即可。
&emsp?2.獨占式同步狀態(tài)獲取與釋放
??通過調(diào)用同步器的 acquire(int arg)方法可以獲取同步狀態(tài), 該方法對中斷不敏感, 也就是由于線程獲取同步狀態(tài)失敗后進入同步隊列中, 后續(xù)對線程進行中斷操作時, 線程不會從 同步隊列中移出。
??3.共享式同步狀態(tài)獲取與釋放
??共享式獲取與獨占式獲取最主要的區(qū)別在于同一時刻能否有多個線程同時 獲取到同步狀態(tài)。 以文件的讀寫為例,如果一個程序在對文件進行讀操作, 那么這一時刻對于該文件的寫操作均被阻塞, 而讀操作能夠同時進行。 寫操作要求對資源的獨占式訪問, 而讀操作可以是共享式訪問, 兩種不同的訪問模式在 同一時刻對文件或資源的訪問情況, 如下圖所示。

??通過調(diào)用同步器的acquireShared(int arg)方法可以共享式地獲取同步狀態(tài)。tryAcquireShared(int arg)方法返回值為 int 類型, 當返回值大于等于 0 時, 表示能夠獲取到同步狀態(tài)。 因此, 在共享式獲取的自旋過程中, 成功獲取到同步狀態(tài)并退出自旋的條件就是 tryAcquireShared(int arg)方法返回值大于等于 0。
??與獨占式一樣, 共享式獲取也需要釋放同步狀態(tài), 通過調(diào)用 releaseShared(int arg)方法可以釋放同步狀態(tài)。
4. 獨占式超時獲取同步狀態(tài)
??通過調(diào)用同步器的 doAcquireNanos(int arg, long nanosTimeout)方法可以超時獲取同步狀態(tài), 即在指定的時間段內(nèi)獲取同步狀態(tài), 如果獲取到同步狀態(tài)則返回 true,否則, 返回 false。該方法提供了傳統(tǒng) Java 同步操作(比如 synchronized 關(guān)鍵字)所不具備的特性。
??在分析該方法的實現(xiàn)前, 先介紹一下響應(yīng)中斷的同步狀態(tài)獲取過程。在 Java 5 之前, 當一個線程獲取不到鎖而被阻塞在 synchronized 之外時, 對該線程進行中斷操作, 此時該線程的中斷標志位會被修改, 但線程依舊會阻塞在 synchronized 上, 等待著獲取鎖。在 Java 5 中, 同步器提供了 acquirelnterruptibly(int arg)方法, 這個方法在等待獲取同步狀態(tài)時, 如果 當前線程被中斷, 會立刻返回, 并拋出 InterruptedException。
??超時獲取同步狀態(tài)過程可以被視作響應(yīng)中斷獲取同步狀態(tài)過程的 “增強版”, doAcquireNanos(int arg, long nanosTimeout)方法在支持響應(yīng)中斷的基礎(chǔ)上, 增加了超時獲取 的特性。針對超時獲取, 主要需要計算出需要睡眠的時間間隔 nanosTimeout,為了防止過早通知, nanosTimeout 計算公式為: nanosTimeout -= now - lastTime,其中 now 為當前喚醒時間, lastTime 為上次喚醒時間, 如果 nanosTimeout 大于 0 則表示超時時間未到, 需要繼續(xù)睡眠 nanosTimeout 納秒, 反之, 表示已經(jīng)超時。
3.重入鎖
??重人鎖 ReentrantLock,顧名思義, 就是支持重進入的鎖, 它表示該鎖能夠支持一個線程對資源的重復(fù)加鎖。 除此之外, 該鎖的還支持獲取鎖時的公平和非公平性選擇。
回憶在同步器一節(jié)中的示例( Mutex), 同時考慮如下場景: 當一個線程調(diào)用 Mutex 的lock()方法獲取鎖之后, 如果再次調(diào)用 lock()方法, 則該線程將會被自己所阻塞, 原因是 Mutex 在實現(xiàn)町rAcquire(int acquires)方法時沒有考慮占有鎖的線程再次獲取鎖的場景, 而在調(diào)用 tryAcquire(int acquires)方法時返回了 false, 導(dǎo)致該線程被阻塞。簡單地說, Mutex 是一個不支持重進入的鎖。 而 synchronized 關(guān)鍵字隱式的支持重進入, 比如一個 synchronized 修飾的遞歸方法, 在方法執(zhí)行時, 執(zhí)行線程在獲取了鎖之后仍能連續(xù)多次地獲得該鎖, 而不像Mutex 由于獲取了鎖, 而在下一次獲取鎖時出現(xiàn)阻塞自己的情況。
??ReentrantLock 雖然沒能像 synchronized 關(guān)鍵字一樣支持隱式的重進入, 但是在調(diào)用lock()方法時, 已經(jīng)獲取到鎖的線程, 能夠再次調(diào)用 lock()方法獲取鎖而不被阻塞。
??這里提到一個鎖獲取的公平性問題 如果在絕對時間上, 先對鎖進行獲取的請求一定先被滿足, 那么這個鎖是公平的, 反之, 是不公平的。 公平的獲取鎖, 也就是等待時間最長的線程最優(yōu)先獲取鎖, 也可以說鎖獲取是順序的。 ReentrantLock 提供了一個構(gòu)造函數(shù), 能夠控制鎖是否是公平的。
??事實上, 公平的鎖機制往往沒有非公平的效率高, 但是, 并不是任何場景都是以TPS作
為唯一的指標, 公平鎖能夠減少 ‘饑餓’ 發(fā)生的概率, 等待越久的請求越是能夠得到優(yōu)先滿足。
1. 實現(xiàn)重進入
??重進入是指任意線程在獲取到鎖之后能夠再次獲取該鎖而不會被鎖所阻塞, 該特性的實現(xiàn)需要解決以下兩個問題。
??1 )錢程再次獲取鎖。 鎖需要去識別獲取鎖的線程是否為當前占據(jù)鎖的線程, 如果是,則再次成功獲取。
??2 )鎖的最終釋煎。 線程重復(fù)n次獲取了鎖, 隨后在第n次釋放該鎖后, 其他線程能夠獲取到該鎖。 鎖的最終釋放要求鎖對于獲取進行計數(shù)自增, 計數(shù)表示當前鎖被重復(fù)獲取的次數(shù), 而鎖被釋放時, 計數(shù)自減, 當計數(shù)等于0時表示鎖已經(jīng)成功釋放。
??ReentrantLock 是通過組合自定義同步器來實現(xiàn)鎖的獲取與釋放, 以非公平性(默認的) 實現(xiàn)。
2.公平與非公平獲取鎖的區(qū)別
??公平鎖是按FIFO的原則。而非公平鎖不按照這個原則。非公平鎖的效率要高,因為它不需要做那么多上下文切換。
4.讀寫鎖
??只需要在讀操作時獲取讀鎖, 寫操作時獲取寫鎖即可。 當寫鎖被獲取到時, 后續(xù)(非當前寫操作線程)的讀寫操作都會被阻塞, 寫鎖釋放之后, 所有操作繼續(xù)執(zhí)行, 編程方式相對于使用等待通知機制的實現(xiàn)方式而言, 變得簡單明了。
??一般情況下, 讀寫鎖的性能都會比排它鎖好, 因為大多數(shù)場景讀是多于寫的。 在讀多于寫的情況下, 讀寫鎖能夠提供比排它鎖更好的并發(fā)性和吞吐量。Java并發(fā)包提供讀寫鎖的實現(xiàn)是 ReentrantReadWriteLock。特性如下:

4.1 讀寫鎖的接口
??ReadWriteLock僅定義了獲取讀鎖和寫鎖的兩個方法, 即readLock()方法和writeLock()方法,而其實現(xiàn)一- ReentrantReadWriteLock, 除了接口方法之外, 還提供了一些便于外界監(jiān)方法, 而其實現(xiàn)
控其內(nèi)部工作狀態(tài)的方法。如下圖:

LockSupport 工具
??LockSupport 定義了一組以park開頭的方法用來阻塞當前線程,以及unpark(Thread thread)方法來喚醒一個被阻塞的線程。

6 Condition 接口
??任意一個 Java 對象, 都擁有一組監(jiān)視器方法(定義在 java.lang.Object 上), 主要包括 wait()、 wait(long timeout)、 notify()以及 notifyAll()方法, 這些方法與 synchronized 同步關(guān)鍵字配合, 可以實現(xiàn)等待/通知模式。 Condition 接口也提供了類似 Object 的監(jiān)視器方法, 與Lock 配合可以實現(xiàn)等待/通知模式 但是這兩者在使用方式以及功能特性上還是有差別的。
通過對比 Object 的監(jiān)視器方法和 Condition 接口, 可以更詳細地了解 Condition 的特性,對比項與結(jié)果如表所示。

6.1 Condition 接口
??Condition 定義了等待/通知兩種類型的方法, 當前線程調(diào)用這些方法時, 需要提前獲取到 Condition 對象關(guān)聯(lián)的鎖。 Condition 對象是由 Lock 對象(調(diào)用 Lock 對象的 newCondition() 方法)創(chuàng)建出來的, 換句話說, Condition 是依賴 Lock 對象的。
??Condition 的使用方式比較簡單, 需要注意在調(diào)用方法前獲取鎖。
??一般都會將 Condition 對象作為成員變量。 當調(diào)用await()方法后, 當前線程會釋放鎖并在此等待, 而其他線程調(diào)用 Condition 對象的 signal()方法, 通知當前線程后,當前線程才從 await()方法返回, 并且在返回前已經(jīng)獲取了鎖。
??Condition定義的(部分)方法以及描述如下圖所示。


??獲取一個 Condition 必須通過 Lock 的 newCondition() 方法。
6.2 Condition 的實現(xiàn)分析
??Condition Object是同步器AbstractQueuedSynchronizer的內(nèi)部類, 因為Condit_ion的操作需要獲取相關(guān)聯(lián)的鎖, 所以作為同步器的內(nèi)部類也較為合理。 每個Condition對象都包含著 一個隊列(以下稱為 等待隊列), 該隊列是Condition對象實現(xiàn)等待/通知功能的關(guān)鍵。
??下面將分析Condition的實現(xiàn), 主要包括: 等待隊列、 等待和通知, 下面提到的Condition如果不加說明均指的是ConditionObject。
??1.等待隊列
??等待隊列是一個FIFO的隊列,在隊列中的每個節(jié)點都包含了一個線程引用,該線程就是在Condition對象上等待的線程,如果一個線程調(diào)用了Condition.await()方法 ,那么該線 程將會釋放鎖、 構(gòu)造成節(jié)點加入等待隊列 并進入等待狀態(tài)。 事實上, 節(jié)點的定義復(fù)用了同
步器中節(jié)點的定義, 也就是說, 同步隊列和 等待隊列中節(jié)點類型都是同步器的靜態(tài)內(nèi)部類AbstractQueuedS ynchronizer.Node。
??一個Condition 包含 一個等待隊列, Condition擁有首節(jié)點( firstWaiter) 和尾節(jié)點( last Waiter)。 當前線程調(diào)用 Condition.await()方法, 將會以當前線程構(gòu)造節(jié)點, 并將節(jié)點從尾部加入等待隊列。
??2.等待
??調(diào)用 Condition 的 await()方法(或者以 await 開頭的方法), 會使當前線程進入等待隊列并釋放鎖, 同時線程狀態(tài)變?yōu)榈却隣顟B(tài)。 當從 await()方法返回時, 當前線程一定獲取了Condition 相關(guān)聯(lián)的鎖。
??如果從隊列(同步隊列和等待隊列) 的角度看 await()方法, 當調(diào)用await()方法時, 相當于同步隊列的首節(jié)點(獲取了鎖的節(jié)點)移動到 Condition 的等待隊列中。
??3. 通知
??調(diào)用Condition 的 signal()方法, 將會喚醒在等待隊列中等待時間最長的節(jié)點(首節(jié)點),在喚醒節(jié)點之前,會將節(jié)點移到同步隊列中。
??調(diào)用該方法的前置條件是當前線程必須獲取了鎖, 可以看到 signal()方法進行了 isHeldExclusively()檢查, 也就是當前線程必須是獲取了鎖的線程。 接著獲取等待隊列的首節(jié)點,將其移動到同步隊列并使用 LockSupport 喚醒節(jié)點中的線程。
??通過調(diào)用同步器的 enq (Node node)方法, 等待隊列中的頭節(jié)點線程安全地移動到同步隊 列。 當節(jié)點移動到同步隊列后, 當前線程再使用 LockSupport 喚醒該節(jié)點的線程。
??被喚醒后的線程, 將從await()方法中的while循環(huán)中退出( isOnSyncQueue (Node node) 方法返回true,節(jié)點已經(jīng)在同步隊列中), 進而調(diào)用同步器的acquir eQueued()方法加入到獲取同步狀態(tài)的競爭中。
??成功獲取同步狀態(tài)(或者說鎖)之后, 被喚醒的線程將從先前調(diào)用的await()方法返回, 此時該線程已經(jīng)成功 地獲取了鎖。
??Condition的signa!All()方法, 相當于對等待隊列中的每個節(jié)點均執(zhí)行一次signal()方法,效果就是將等待隊列中所有節(jié)點全部移動到同步隊列中, 并喚醒 每個節(jié)點的線程。