Java并发编程—JUC的Lock锁
?
一、Lock (JUC鎖)
JUC 鎖位于java.util.concurrent.locks包下,為鎖和等待條件提供一個框架,它不同于內置同步和監視器。
CountDownLatch,CyclicBarrier 和 Semaphore 不在包中屬于并發編程中的工具類,但也是通過 AQS(后面會講) 來實現的。因此,我也將它們歸納到 JUC 鎖中進行介紹。
| 1、Lock? | Lock實現提供了比使用synchronized方法和語句可獲得的更廣泛的鎖定操作。 |
| 2、ReentrantLock | 一個可重入的互斥鎖,它具有與隱式鎖synchronized相同的一些基本行為和語義,但功能更強大。 |
| 3、AQS類 | AbstractOwnableSynchronizer/AbstractQueuedSynchronizer/AbstractQueuedLongSynchronizer AbstractQueuedSynchronizer 就是被稱之為AQS的類,為實現依賴于先進先出 (FIFO) 等待隊列的阻塞鎖和相關同步器(信號量、事件,等等)提供一個框架。ReentrantLock,ReentrantReadWriteLock,CountDownLatch,CyclicBarrier和Semaphore等這些類都是基于AQS類實現的。? AbstractOwnableSynchronizer是可以由線程以獨占方式擁有的同步器。 |
| 4、Condition? | Condition又稱等待條件,它實現了對鎖更精確的控制。 Condition中的await()方法相當于Object的wait()方法,Condition中的signal()方法相當于Object的notify()方法,Condition中的signalAll()相當于Object的notifyAll()方法。 不同的是,Object中的wait(),notify(),notifyAll()方法是和synchronized組合使用的;而Condition需要與Lock組合使用。 |
| 5、ReentrantReadWriteLock? | ReentrantReadWriteLock維護了一對相關的鎖,一個用于只讀操作,另一個用于寫入操作。 |
| 6、LockSupport | 用來創建鎖和其他同步類的基本線程阻塞原語。 |
| 7、CountDownLatch? | 一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。 |
| 8、CyclicBarrier? | 一個同步輔助類,它允許一組線程互相等待,直到到達某個公共屏障點。 |
| 9、Semaphore? | 一個計數信號量。從概念上講,信號量維護了一個許可集。Semaphore通常用于限制可以訪問某些資源的線程數目。 |
二、Lock與ReentrantLock
1、概述
Java中的鎖有兩種,synchronized與Lock。因為使用synchronized并不需要顯示地加鎖與解鎖,所以往往稱synchronized為隱式鎖,而使用Lock時則相反,所以一般稱Lock為顯示鎖。synchronized修飾方法或語句塊,所有鎖的獲取和釋放都必須出現在一個塊結構中。當需要靈活地獲取或釋放鎖時,synchronized顯然是不符合要求的。Lock接口的實現允許鎖在不同的范圍內獲取和釋放,并支持以任何順序獲取和釋放多個鎖。一句話,Lock實現比synchronized更靈活。但凡事有利就有弊,不使用塊結構鎖就失去了使用synchronized修飾方法或語句時會出現的鎖自動釋放功能,在大多數情況下,Lock實現需要手動釋放鎖。除了更靈活之外,Lock還有以下優點:
- Lock 實現提供了使用 synchronized 方法和語句所沒有的其他功能,包括提供了一個非塊結構的獲取鎖嘗試?tryLock()、一個獲取可中斷鎖的嘗試?lockInterruptibly()?和一個獲取超時失效鎖的嘗試?tryLock(long, TimeUnit)。
- Lock 類還可以提供與隱式監視器鎖完全不同的行為和語義,如保證排序、非重入用法或死鎖檢測。如果某個實現提供了這樣特殊的語義,則該實現必須對這些語義加以記錄。
ReentrantLock是一個可重入的互斥鎖。顧名思義,“互斥鎖”表示在某一時間點只能被同一線程所擁有。“可重入”表示鎖可被某一線程多次獲取。當然 synchronized 也是可重入的互斥鎖。當鎖沒有被某一線程占有時,調用 lock() 方法的線程將成功獲取鎖。可以使用isHeldByCurrentThread()和?getHoldCount()方法來判斷當前線程是否擁有該鎖。
ReentrantLock既可以是公平鎖又可以是非公平鎖。當此類的構造方法 ReentrantLock(boolean fair) 接收true作為參數時,ReentrantLock就是公平鎖,線程依次排隊獲取公平鎖,即鎖將被等待最長時間的線程占有。與默認情況(使用非公平鎖)相比,使用公平鎖的程序在多線程環境下效率比較低。而且公平鎖不能保證線程調度的公平性,tryLock方法可在鎖未被其他線程占用的情況下獲得該鎖。
2、API
1、構造方法
//創建一個 ReentrantLock 的實例。 ReentrantLock() //創建一個具有給定公平策略的 ReentrantLock。 ReentrantLock(boolean fair)2、方法摘要
int getHoldCount() //查詢當前線程保持此鎖的次數。 protected Thread getOwner() //返回目前擁有此鎖的線程,如果此鎖不被任何線程擁有,則返回 null。 protected Collection<Thread> getQueuedThreads() //返回一個 collection,它包含可能正等待獲取此鎖的線程。int getQueueLength() //返回正等待獲取此鎖的線程估計數。 protected Collection<Thread> getWaitingThreads(Condition condition) //返回一個 collection,它包含可能正在等待與此鎖相關給定條件的那些線程。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() //如果當前線程未被中斷,則獲取鎖。Condition newCondition() //返回用來與此 Lock 實例一起使用的 Condition 實例。String toString() //返回標識此鎖及其鎖定狀態的字符串。boolean tryLock() //僅在調用時鎖未被另一個線程保持的情況下,才獲取該鎖。boolean tryLock(long timeout, TimeUnit unit) //如果鎖在給定等待時間內沒有被另一個線程保持,且當前線程未被中斷,則獲取該鎖。void unlock() //試圖釋放此鎖。3、代碼
1、典型的代碼
class X {private final ReentrantLock lock = new ReentrantLock();// ...public void m() { lock.lock(); // block until condition holdstry {// ... method body} finally {lock.unlock()}} }2、買票
public class SellTickets {public static void main(String[] args) {TicketsWindow tw1 = new TicketsWindow();Thread t1 = new Thread(tw1, "一號窗口");Thread t2 = new Thread(tw1, "二號窗口");t1.start();t2.start();} }class TicketsWindow implements Runnable {private int tickets = 1;private final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock();try {if (tickets > 0) {System.out.println(Thread.currentThread().getName() + "還剩余票:" + tickets + "張");--tickets;System.out.println(Thread.currentThread().getName() + "賣出一張火車票,還剩" + tickets + "張");} else {System.out.println(Thread.currentThread().getName() + "余票不足,暫停出售!");try {Thread.sleep(1000 * 60);} catch (InterruptedException e) {e.printStackTrace();}}} finally {lock.unlock();}}} }4、總結
- 與synchronized 相比ReentrantLock的使用更靈活。Lock接口的實現允許鎖在不同的范圍內獲取和釋放,并支持以任何順序獲取和釋放多個鎖。
- ReentrantLock具有與使用 synchronized 相同的一些基本行為和語義,但功能更強大。包括提供了一個非塊結構的獲取鎖嘗試?tryLock()、一個獲取可中斷鎖的嘗試?lockInterruptibly()?和一個獲取超時失效鎖的嘗試?tryLock(long, TimeUnit)。
- ReentrantLock 具有 synchronized 所沒有的許多特性,比如時間鎖等候、可中斷鎖等候、無塊結構鎖、多個條件變量或者輪詢鎖。
- ReentrantLock 可伸縮性強,應當在高度爭用的情況下使用它。
三、AQS
1、概述
談到 ReentrantLock,不得不談?AbstractQueuedSynchronizer(AQS)!AQS,AbstractQueuedSynchronizer的縮寫(抽象的隊列式的同步器),是 JUC 的核心。AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現都依賴于它,如常用的ReentrantLock、Semaphore、CountDownLatch。
它維護了一個?volatile int state(代表共享資源)和一個?FIFO 線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)
這里 volatile 是核心關鍵詞,具體volatile的語義,在此不述。state的訪問方式有三種:
- getState()
- setState()
- compareAndSetState()
AQS定義兩種資源共享方式:Exclusive(獨占鎖,只有一個線程能執行,如 ReentrantLock)和?Share(共享鎖,多個線程可同時執行,如Semaphore/CountDownLatch)。AQS的子類(鎖或者同步器)實現時只需要實現共享資源state的獲取與釋放方式即可,至于具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:
- isHeldExclusively():該線程是否正在獨占資源。只有用到condition才需要去實現它。
- tryAcquire(int):獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false。
- tryRelease(int):獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false。
- tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩余可用資源;正數表示成功,且有剩余資源。
- tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放后允許喚醒后續等待結點返回true,否則返回false。
以 ReentrantLock 為例,state 初始化為 0,表示未鎖定狀態。A 線程?lock()?時,會調用tryAcquire()獨占該鎖并將 state+1。此后,其他線程再 tryAcquire() 時就會失敗,直到 A 線程 unlock() 到 state=0(即釋放鎖)為止,其它線程才有機會獲取該鎖。當然,釋放鎖之前,A線程自己是可以重復獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多么次,這樣才能保證state是能回到零態的。
再以 CountDownLatch 以例,任務分為 N 個子線程去執行,state 也初始化為 N(注意N要與線程個數一致)。這N個子線程是并行執行的,每個子線程執行完后countDown()一次,state會CAS減1。等到所有子線程都執行完后(即state=0),會 unpark() 主線程,然后主線程就會從 await() 函數返回,繼續后余動作。
一般來說,自定義同步器要么是獨占方法,要么是共享方式,他們也只需實現 獲取-釋放資源 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可。但AQS也支持自定義同步器同時實現獨占和共享兩種方式,如?ReentrantReadWriteLock。
2、總結
在AQS的設計中,在父類AQS中實現了對 等待隊列的默認實現,子類中幾乎不用修改該部分功能。而state在子類中根據需要被賦予了不同的意義,子類通過對state的不同操作來提供不同的同步器功能,進而對封裝的工具類提供不同的功能。AQS核心思想是,如果被請求的共享資源空閑,則將當前請求資源的線程設置為有效的工作線程,并且將共享資源設置為鎖定狀態。如果被請求的共享資源被占用就將 暫時獲取不到鎖的線程加入到隊列中(CLH隊列 自旋 雙向隊列),AQS是將每條請求共享資源的線程封裝成一個CLH鎖隊列的一個結點(Node)來實現鎖的分配。
四、Condition
在任務協作中,關鍵問題是任務之間的通信,除了 同步監視器之外,Java 1.5 之后還提供了 Lock 跟 Condition 組合來實現線程之間的通信
1、與Object監視器監視器方法的比較
| 使用條件 | 獲取鎖 | 獲取鎖,創建Condition對象 |
| 等待隊列的個數 | 一個 | 多個 |
| 是否支持通知指定等待隊列 | 支持 | 不支持 |
| 是否支持當前線程釋放鎖進入等待狀態 | 支持 | 支持 |
| 是否支持當前線程釋放鎖并進入超時等待狀態 | 支持 | 支持 |
| 是否支持當前線程釋放鎖并進入等待狀態直到指定最后期限 | 支持 | 不支持 |
| 是否支持喚醒等待隊列中的一個任務 | 支持 | 支持 |
| 是否支持喚醒等待隊列中的全部任務 | 支持 | 支持 |
2、API
void await() //造成當前線程在接到信號或被中斷之前一直處于等待狀態。boolean await(long time, TimeUnit unit) //造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態。long awaitNanos(long nanosTimeout) //造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態。void awaitUninterruptibly() //造成當前線程在接到信號之前一直處于等待狀態。boolean awaitUntil(Date deadline) //造成當前線程在接到信號、被中斷或到達指定最后期限之前一直處于等待狀態。void signal() //喚醒一個等待線程。void signalAll() //喚醒所有等待線程。3、演示下Condition是如何更精細地控制線程的休眠與喚醒的。
public class BoundedBuffer {final Lock lock = new ReentrantLock();//鎖final Condition notFull = lock.newCondition();//寫條件final Condition notEmpty = lock.newCondition();//讀條件final Object[] items = new Object[100];int putptr, takeptr, count;//存數據public void put(Object x) throws InterruptedException {lock.lock();try {while (count == items.length)//如果隊列已滿notFull.await();//阻塞寫線程items[putptr] = x;if (++putptr == items.length)putptr = 0;++count;notEmpty.signal();//喚醒讀線程} finally {lock.unlock();}}//寫數據public Object take() throws InterruptedException {lock.lock();try {while (count == 0)//如果隊列已空notEmpty.await();//阻塞讀線程Object x = items[takeptr];if (++takeptr == items.length)takeptr = 0;--count;notFull.signal();//喚醒寫線程return x;} finally {lock.unlock();}} }這是一個有界的緩沖區,支持put(Object)與take()方法。put(Object)負責向緩沖區中存數據,take負責從緩沖區中讀數據。在多線程環境下,調用put(Object)方法,當緩沖區已滿時,會阻塞寫線程,如果緩沖區不滿,則寫入數據,并喚醒讀線程。調用take()方法時,當緩沖區為空,會阻塞讀線程,如果緩沖區不空,則讀取數據,并喚醒寫線程。
這就是多個Condition的強大之處,假設緩存隊列已滿,那么阻塞的肯定是寫線程,喚醒的肯定是讀線程,相反,阻塞的肯定是讀線程,喚醒的肯定是寫線程。如果采用Object類中的wait(), notify(), notifyAll()實現該緩沖區,當向緩沖區寫入數據之后需要喚醒讀線程時,通過notify()或notifyAll()無法明確的指定喚醒讀線程,而只能通過notifyAll喚醒所有線程,但notifyAll無法區分喚醒的線程是讀線程,還是寫線程。 如果喚醒的是寫線程,那么線程剛被喚醒,又被阻塞了,這樣就降低了效率。
五、ReentrantReadWriteLock
1、概述
ReentrantLock是互斥鎖。與互斥鎖相對應的是共享鎖。ReadWriteLock就是一種共享鎖 ,ReentrantReadWriteLock是支持與 ReentrantLock 類似語義的 ReadWriteLock 實現。ReadWriteLock 維護了兩個鎖,讀鎖和寫鎖,所以一般稱其為讀寫鎖。寫鎖是獨占的。讀鎖是共享的,如果沒有寫鎖,讀鎖可以由多個線程共享。與互斥鎖相比,雖然一次只能有一個寫線程可以修改共享數據,但大量讀線程可以同時讀取共享數據,所以,在共享數據很大,且讀操作遠多于寫操作的情況下,讀寫鎖值得一試。
ReadWriteLock源碼如下:
public interface ReadWriteLock {//返回用于讀取操作的鎖。Lock readLock();//返回用于寫入操作的鎖。Lock writeLock(); }從源碼中可以看到,ReadWriteLock并不是Lock的子接口。所以ReadWriteLock并沒有Lock的那些特性。
2、使用場景
在使用某些種類的Collection時,可以使用ReentrantReadWriteLock來提高并發性。通常,在預期collection很大,讀取者線程訪問它的次數多于寫入者線程,并且entail操作的開銷高于同步開銷時,這很值得一試。例如,以下是一個使用 TreeMap的類,預期它很大,并且能被同時訪問。
public class RWDictionary {// TeepMap就是讀的多,插入的少的場景private final Map<String, Data> m = new TreeMap<String, Data>();private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();//讀鎖private final Lock r = rwl.readLock();//寫鎖private final Lock w = rwl.writeLock();public Data get(String key) {r.lock();try {return m.get(key);} finally {r.unlock();}}public String[] allKeys() {r.lock();try {return (String[])m.keySet().toArray();} finally {r.unlock();}}public Data put(String key, Data value) {w.lock();try {return m.put(key, value);} finally {w.unlock();}}public void clear() {w.lock();try {m.clear();} finally {w.unlock();}} }3、特性
ReentrantReadWriteLock 具有以下特性:(有待詳細介紹)
- 公平性
- 重入性
- 鎖降級
- 鎖獲取中斷
- 支持Condition
- 檢測系統狀態
優點?
與互斥鎖相比,雖然一次只能有一個寫線程可以修改共享數據,但大量讀線程可以同時讀取共享數據。在共享數據很大,且讀操作遠多于寫操作的情況下,ReentrantReadWriteLock值得一試。
缺點?
只有當前沒有線程持有讀鎖或者寫鎖時才能獲取到寫鎖,這可能會導致寫線程發生饑餓現象,即讀線程太多導致寫線程遲遲競爭不到鎖而一直處于等待狀態。StampedLock?可以解決這個問題,解決方法是如果在讀的過程中發生了寫操作,應該重新讀而不是直接阻塞寫線程。
六、StampedLock
1、概述
StampedLock是JDK1.8新增的一個鎖,是對讀寫鎖ReentrantReadWriteLock的改進。前面已經學習了ReentrantReadWriteLock,我們了解到,在共享數據很大,且讀操作遠多于寫操作的情況下,ReentrantReadWriteLock 值得一試。但要注意的是,只有當前沒有線程持有讀鎖或者寫鎖時才能獲取到寫鎖,這可能會導致寫線程發生饑餓現象,即讀線程太多導致寫線程遲遲競爭不到鎖而一直處于等待狀態。StampedLock可以解決這個問題,解決方法是如果在讀的過程中發生了寫操作,應該重新讀而不是直接阻塞寫線程。
StampedLock有三種讀/寫模式:寫、讀、樂觀讀。
- 寫。獨占鎖,只有當前沒有線程持有讀鎖或者寫鎖時才能獲取到該鎖。方法writeLock()返回一個可用于unlockWrite(long)釋放鎖的方法的戳記。tryWriteLock()提供不計時和定時的版本。
- 讀。共享鎖,如果當前沒有線程持有寫鎖即可獲取該鎖,可以由多個線程獲取到該鎖。方法readLock()返回可用于unlockRead(long)釋放鎖的方法的戳記。tryReadLock()也提供不計時和定時的版本。
- 樂觀讀。方法tryOptimisticRead()僅當鎖定當前未處于寫入模式時,方法才會返回非零戳記。返回戳記后,需要調用validate(long stamp)方法驗證戳記是否可用。也就是看當調用tryOptimisticRead返回戳記后到到當前時間是否有其他線程持有了寫鎖,如果有,返回false,否則返回true,這時就可以使用該鎖了。
2、代碼
StampedLock 則提供了一種樂觀的讀策略,這種樂觀策略的鎖非常類似于無鎖的操作,使得樂觀鎖完全不會阻塞寫線程
class Point {private double x, y;private final StampedLock sl = new StampedLock();/*** 改變當前坐標。* 先獲取寫鎖,然后對point坐標進行修改,最后釋放鎖。* 該鎖是排它鎖,這保證了其他線程調用move函數時候會被阻塞,直到當前線程顯示釋放了該鎖。*/void move(double deltaX, double deltaY) { // an exclusively locked methodlong stamp = sl.writeLock();try {x += deltaX;y += deltaY;} finally {sl.unlockWrite(stamp);}}/*** 計算當前坐標到原點的距離* * @return*/double distanceFromOrigin() { //1.嘗試獲取樂觀讀鎖,返回stamplong stamp = sl.tryOptimisticRead();//2.拷貝參數到本地方法棧中double currentX = x, currentY = y;//3.驗證stamp是否有效if (!sl.validate(stamp)) {//4.如果stamp無效,說明得到stamp后,又有其他線程獲得了寫鎖//5.獲取讀鎖stamp = sl.readLock();try {//6.其他線程修改了x,y的值,為了數據的一致性,需要再次再次拷貝參數到本地方法棧中currentX = x;currentY = y;} finally {//7.釋放讀鎖sl.unlockRead(stamp);}}//8.使用參數的拷貝來計算當前坐標到原點的距離。無論步驟3中stamp有沒有驗證成功//,參數的拷貝都是當前坐標的值return Math.sqrt(currentX * currentX + currentY * currentY);}/*** 如果當前坐標為原點則移動到指定的位置*/void moveIfAtOrigin(double newX, double newY) { // upgrade// 獲取讀鎖,保證其他線程不能獲取到寫鎖long stamp = sl.readLock();try {//如果當前坐標為原點while (x == 0.0 && y == 0.0) {//嘗試升級成寫鎖long ws = sl.tryConvertToWriteLock(stamp);//如果升級成功,更新坐標值if (ws != 0L) {stamp = ws;x = newX;y = newY;break;} else {//如果升級成功sl.unlockRead(stamp);//先釋放讀鎖stamp = sl.writeLock();//再獲取寫鎖//循環while中的操作,直到成功更新坐標值}}} finally {//最后釋放寫鎖sl.unlock(stamp);}} }3、StampedLock 原理
StampedLock的內部實現是基于CLH鎖的,CLH鎖是一種自旋鎖,它保證沒有饑餓的發生,并且可以保證FIFO(先進先出)的服務順序.CLH鎖的基本思想如下:鎖維護一個等待線程隊列,所有申請鎖,但是沒有成功的線程都記錄在這個隊列中,每一個節點代表一個線程,保存一個標記位(locked).用與判斷當前線程是否已經釋放鎖;locked=true 沒有獲取到鎖,false 已經成功釋放了鎖
當一個線程視圖獲得鎖時,取得等待隊列的尾部節點作為其前序節點.并使用類似如下代碼判斷前序節點是否已經成功釋放鎖:只要前序節點(pred)沒有釋放鎖,則表示當前線程還不能繼續執行,因此會自旋等待,
反之,如果前序線程已經釋放鎖,則當前線程可以繼續執行.釋放鎖時,也遵循這個邏輯,線程會將自身節點的locked位置標記位false,那么后續等待的線程就能繼續執行了七、CountDownLatch、Semaphore、Exchanger、CyclicBarrier
博客地址
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的Java并发编程—JUC的Lock锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java并发编程—锁的基本概念
- 下一篇: Java并发编程—自旋锁CLHLock原