java中的鎖

java中的鎖

1. java加鎖的方式

  java中的鎖用來保證數據的可見性,一個鎖能能夠防止多個線程同時訪問共享資源。
  Java提供兩種加鎖的方式:

  1. 通過synchronized關鍵字實現鎖功能。

  2. 通過Lock接口(jdk 1.5之後支持)

1.1 兩者的區別

  1. synchronized可以隱式的實現獲取和釋放鎖,Lock必須要在同步代碼塊前後顯示的進行鎖的獲取和釋放。

  2. Lock接口提高了對鎖的可操作性,支持可中斷的獲取鎖以及循環獲取鎖等多種sychronized關鍵字不具備的特性。

  例如:針對一個場景,手把手進行鎖獲取和釋放,先獲得鎖A,然後再獲取鎖B,當鎖B獲得後,釋放鎖A同時獲取鎖C,當鎖C獲得後,再釋放B同時獲取鎖D,以此類推。這種場景下,synchronized關鍵字就不那麼容易實現了,而使用Lock卻容易許多。
在這裏插入圖片描述

2. 鎖的使用方法:

2.1 sychonized

sychonized的使用方式之前介紹過,不再重複

2.2 Lock

Lock lock=new ReentrantLock();//重入鎖,鎖的一種實現
lock.lock();
try{
    代碼塊...
}finally{
		lock.unlock();
}

LOCK的API

方法名稱 描述
void lock(); 調用該方法後線程嘗試獲取鎖,當鎖獲得後,從該方法返回。否則線程阻塞。
void lockInterruptibly() throws InterruptedException; 可中斷的獲取鎖,和lock()的區別在於在獲取的過程中能線程能響應中斷。
boolean tryLock(); 嘗試非阻塞的獲取鎖,調用該方法後立刻返回,如果能夠獲取則放回true,否則返回false。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 超時的獲取鎖,當前線程在以下三種情況會返回:
1. 當前線程在超時時間內獲得了鎖。
2. 當前線程在超時時間內被中斷。
3. 超時時間結束,返回false。
void unlock(); 釋放鎖
Condition newCondition(); 獲取等待通知組件,該組件和當前的鎖綁定,當前線程只用獲得了鎖,才能調用該組件的wait()方法,而調後,當前線程會釋放鎖。
注:實際是對java通知等待模式的一種封裝,在之前java線程介紹中,有解釋過怎麼調用對象的wait()和notify()實現等待通知機制。

  這裏先簡單介紹一下Lock接口的API,隨後的章節會詳細介紹同步器AbstractQueuedSynchronizer以及常用Lock接口的實現ReentrantLock。Lock接口的實現基本都是通過聚合了一個同步器的子類來完成線程訪問控制的。

3. 隊列同步器

  隊列同步器AbstractQueuedSynchronizer(AQS),是一個用來創建鎖或者其他同步組件的基礎框架,它使用了一個int 成員變量status來表示同步狀態,通過內置的FIFO(先入先出隊列)來完成資源獲取線程的排隊工作。

  同步器的字類推薦被定義爲自定義同步組件的內部靜態類,同步器暴露了幾個抽象方法讓程序員實現,而加鎖的過程中會使用到者幾個關鍵的抽象方法,從而來管理同步狀態,實現自定義效果的同步組件。同步器支持獨佔式的獲取同步狀態,也支持共享式的獲取同步狀態。

3.1 AQS的接口與示例

同步器通過三個方法來防衛或者修改同步狀態

private volatile int state;
  • getStatus() :獲取同步狀態

    protected final int getState() {
            return state;
    }
    
  • setStats(int newStatus):設置同步狀態

    protected final void setState(int newState) {
            state = newState;
        }
    
  • compareAndSetStatsu(int expece,int update):使用CAS設置當前狀態,該方法能保證狀態設置的原子性。

     protected final boolean compareAndSetState(int expect, int update) {
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
        }
    

    同步器暴露給開發者的幾個方法:

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

      AQS設計者採用了模板模式來實現自定義組件的開發,例如,重入鎖的公平鎖實現加鎖lock()方法會調用acquire(int newStatus)

      static final class FairSync extends Sync {
      
              final void lock() {
                  acquire(1);
              }
          ...
      }
      

      而acquire會調用tryAcquire(int arg)方法:

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

      程序員通過自己實現的tryAcquire()方法,從而實現自定義組件的加鎖特性。其他的模板方法如下:

      1557109720122

  同步器提供的模板方法基本上分爲3類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步隊列中的等待線程情況。自定義同步組件將使用同步器提供的模板方法來實現自己的同步語義。

3.2使用AQS實現自定義組件獨佔鎖

package xin.spring.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class Mutex implements Lock {
	private static class syn extends AbstractQueuedSynchronizer{
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;
		//判斷是否處於佔用狀態
		protected boolean isHeldExclusively(){
			return getState()==1;      //if the state==1, mean the lock being monopolized
		}
		protected boolean tryAcquire(int acquired){
			if(compareAndSetState(0, 1)){
                //設置當前線程爲獨佔線程
				setExclusiveOwnerThread(Thread.currentThread());
				return true;
			}
			return false;
		}
		protected boolean tryRelease(int releases){
			if(getState()==0){
				throw new IllegalMonitorStateException("can not release");
			}
			setState(0);
            //設置獨佔線程爲Null
			setExclusiveOwnerThread(null);
			return true;
		}
		
	}
	syn syn=new syn();
	@Override
	public void lock() {
		syn.acquire(1);
	}

	@Override
	public void lockInterruptibly() throws InterruptedException {
		syn.acquireInterruptibly(1);
	}

	@Override
	public boolean tryLock() {
		return syn.tryAcquire(1);
	}

	@Override
	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		return syn.tryAcquireNanos(1, unit.toNanos(time));
	}

	@Override
	public void unlock() {
		syn.release(1);
		
	}

	@Override
	public Condition newCondition() {
		return null;
	}

}

  上述示例中,獨佔鎖Mutex是一個自定義同步組件,它在同一時刻只允許一個線程佔有鎖。Mutex中定義了一個靜態內部類,該內部類繼承了同步器並實現了獨佔式獲取和釋放同步狀態。在tryAcquire(int acquires)方法中,如果經過CAS設置成功(同步狀態設置爲1),則代表獲取了同步狀態,而在tryRelease(int releases)方法中只是將同步狀態重置爲0。用戶使Mutex時並不會直接和內部同步器的實現打交道,而是調用Mutex提供的方法,在Mutex的實現中,以獲取鎖的lock()方法爲例,只需要在方法實現中調用同步器的模板方法acquire(int args)即可,當前線程調用該方法獲取同步狀態失敗後會被加入到同步隊列中等待,這樣就大大降低了實現一個可靠自定義同步組件的門檻。

4 .隊列同步器的實現分析

  接下來將從實現角度分析同步器是如何完成線程同步的,主要包括:同步隊列、獨佔式同步狀態獲取與釋放、共享式同步狀態獲取與釋放以及超時獲取同步狀態等同步器的核心數據結構與模板方法。

4.1 同步隊列

  同步隊列是一個服從FIFO的鏈表隊列,當線程獲取鎖失敗後,將會被構造成一個節點Node並加入到同步隊列中,同時會阻塞當前線程。當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。AQS維護頭節點head和尾節點tail。

4.1.1 Node

Node是AQS中的一個靜態內部類,是同步隊列和等待隊列中公用的一個節點類,成員變量有:

  1. 當前獲取鎖的線程:volatile Thread thread;

  2. 後續節點:volatile Node next;

  3. 前繼節點:volatile Node prev;

  4. 等待隊列中的後繼節點:Node nextWaiter;如果當前節點是共享的,那麼這個字段是一個SHARED常量,也就是說節點類型(獨佔或者共享)和等待隊列中的後繼節點共用同一個字段。

  5. 等待狀態:volatile int waitStatus;

    分別有:

    //由於在同步隊列中等待的線程等待超時或者被中斷,需要從同步隊列中取消等待,節點進入該狀體將不會變化
    static final int CANCELLED =  1;
    //後繼節點的線程處於等待狀態,而當前節點的線程如果釋放了鎖或者被取消後需要通知後續節點,使後續節點得以運行。
     /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    //當前節點在等待隊列中,節點線程等待在condition對象上,當其他線程調用了condition對象的signal方法後,節點會從等待隊列轉移到同步隊列中嘗試獲取鎖
    static final int CONDITION = -2;
    //表示下一次共享式同步狀態會被無條件的傳播下去
    static final int PROPAGATE = -3;
    
  6. ///** Marker to indicate a node is waiting in shared mode */
    //SHREAD常量
    static final Node SHARED = new Node();
    
  7. /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;
    

4.1.2隊列結構

在這裏插入圖片描述
  當線程獲取鎖失敗後,會被加入到同步隊列中來,爲了保證線程安全,因此同步器提供了一個基於CAS的設置尾節點的方法來compareAndSetTail(Node expect,Node update);

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

4.2.1 獲取

源碼分析:

  通過調用同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是由於線程獲取同步狀態失敗後進入同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移出。

//獲取鎖
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//失敗則添加到同步隊列,並調用acquireQueued實現自旋獲取
        selfInterrupt();
}

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
    	//嘗試一次快速的加入隊列,並修改尾節點,允許失敗,失敗後調用end方法
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //調用CAS方法將當前節點設置爲尾節點
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

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

  最後調用acquireQueued(Node node,int arg)方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。

acquireQueued(Node node ,int newStatus)

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)) {
                    //設置首節點,因爲已經獲取到了鎖,所以不需要使用CAS
                    setHead(node);
                    p.next = null; // help GC,方便垃圾回收
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) //調用park方法進行阻塞
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

  在**acquireQueued(final Node node,int arg)**方法中,當前線程在“死循環”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能夠嘗試獲取同步狀態,這是爲什麼?原因有兩個,如下。
第一,頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態之後,將會
喚醒其後繼節點,後繼節點的線程被喚醒後需要檢查自己的前驅節點是否是頭節點。
第二,維護同步隊列的FIFO原則。該方法中,節點自旋獲取同步狀態的行爲如圖5-4所示。
在這裏插入圖片描述
  在圖5-4中,由於非首節點線程前驅節點出隊或者被中斷而從等待狀態返回,隨後檢查自
己的前驅是否是頭節點,如果是則嘗試獲取同步狀態。可以看到節點和節點之間在循環檢查
的過程中基本不相互通信,而是簡單地判斷自己的前驅是否爲頭節點,這樣就使得節點的釋
放規則符合FIFO,並且也便於對過早通知的處理(過早通知是指前驅節點不是頭節點的線程
由於中斷而被喚醒)。獨佔式同步狀態獲取流程,也就是acquire(int arg)方法調用流程,如下圖所示。
在這裏插入圖片描述

4.2.2 釋放

通過調用同步器的release方法就能釋放同步狀態。

源碼分析:

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

釋放後,會調用unparkSuccessor(Node head)方法,喚醒後繼節點。

private void unparkSuccessor(Node node) {
        
        int ws = node.waitStatus;
    //如果狀態爲負(即,可能需要signal),則嘗試清除預期信令。 如果失敗或者等待線程改變了狀態,則可以。
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
     //需要喚醒在後續節點中,它通常只是下一個節點。但是,如果下一個節點爲空或者狀態是CANCELLED(1),則從tail向後遍歷以找到實際的非CANCELLED後繼。
        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);
    }

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

共享式狀態與獨佔式的區別是是否允許多個線程同時獲取鎖。

4.3.1 獲取

通過調用AQS的acquireShared方法獲取同步狀態

public final void acquireShared(int arg) {
    //小於0表示獲取共享鎖失敗
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

doAcquireShared

private void doAcquireShared(int arg) {
        //添加到同步隊列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋獲取鎖
            for (;;) {
                final Node p = node.predecessor();
                //如果前繼節點是head節點
                if (p == head) {
                    //嘗試獲取共享鎖,如果返回值>0,則表示獲取成功
                    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);
        }
    }

4.3.2 釋放

調用tryReleaseShared進行共享鎖的釋放

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

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

4.4 獨佔式超時獲取同步狀態

  在分析該方法的實現前,先介紹一下響應中斷的同步狀態獲取過程。在Java 5之前,當一個線程獲取不到鎖而被阻塞在synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標誌位會被修改,但線程依舊會阻塞在synchronized上,等待着獲取鎖。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法,這個方法在等待獲取同步狀態時,如果當前線程被中斷,會立刻返回,並拋出InterruptedException。

源碼分析:

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            // 獲取失敗,則調用doAcquireInterruptibly(int arg)方法
            doAcquireInterruptibly(arg)
    }

doAcquireInterruptibly(int arg)

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //如果線程被中斷,拋出異常
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

超時獲取同步狀態過程可以被視作響應中斷獲取同步狀態過程的**“增強版”,**
doAcquireNanos(int arg,long nanosTimeout)方法在支持響應中斷的基礎上,增加了超時獲取的特性。針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,爲了防止過早通知,nanosTimeout計算公式爲:nanosTimeout-=now-lastTime,其中now爲當前喚醒時間,lastTime爲上次喚醒時間,如果nanosTimeout大於0則表示超時時間未到,需要繼續睡眠nanosTimeout納秒,反之,表示已經超時。

調用順序:tryAcquireNanos——>doAcquireNanos

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

doAcquireNanos(arg, nanosTimeout);

private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        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;
                }
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

作者使用了一個nanosTimeout表示還要等待的時間,如果nanosTimeout<0後還沒有獲取到鎖,則放回false,如果nanosTimeout > spinForTimeoutThreshold,使用超時park(方法),當nanosTimeout <spinForTimeoutThreshold後,如果再使用park方法,或導致時間不精確的問題,因此會使用快速自旋的方法來計時。
在這裏插入圖片描述

5 LockSupport工具

回顧第4章,當需要阻塞或喚醒一個線程的時候,都會使用LockSupport工具類來完成相應
工作。LockSupport定義了一組的公共靜態方法,這些方法提供了最基本的線程阻塞和喚醒功能,而LockSupport也成爲構建同步組件的基礎工具。LockSupport定義了一組以park開頭的方法用來阻塞當前線程,以及unpark(Thread thread)方法來喚醒一個被阻塞的線程。Park有停車的意思,假設線程爲車輛,那麼park方法代表着停車,而unpark方法則是指車輛啓動離開,這些方法以及描述如表5-10所示。
在這裏插入圖片描述
在Java 6中,LockSupport增加了park(Object blocker)、parkNanos(Object blocker,long nanos)和parkUntil(Object blocker,long deadline)3個方法,用於實現阻塞當前線程的功能,其中參數blocker是用來標識當前線程在等待的對象(以下稱爲阻塞對象),該對象主要用於問題排查和系統監控。

6 Condition接口

再之前講述java線程 Java併發編程基礎之線程詳解的5.3講到過等待/通知機制。而Condition是另外一種實現等待/通知機制的方法。當線程調用到這些方法時,需要先獲取到Condition對象關聯的鎖。Condition對象是由Lock對象(調用Lock對象的newCondition()方法創
建出來的,換句話說,Condition是依賴Lock對象的。其部分方法如5-13所示.
在這裏插入圖片描述
使用示例,使用condition構造一個有界隊列

public class BoundedQueue<T> {
    private Object[] items;
    // 添加的下標,刪除的下標和數組當前數量
    private int addIndex, removeIndex, count;
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();
    public BoundedQueue(int size) {
        items = new Object[size];
    }
    // 添加一個元素,如果數組滿,則添加線程進入等待狀態,直到有"空位"
    public void add(T t) throws InterruptedException {
        //先加鎖
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[addIndex] = t;
            if (++addIndex == items.length)
                addIndex = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
    // 由頭部刪除一個元素,如果數組空,則刪除線程進入等待狀態,直到有新添加元素
    @SuppressWarnings("unchecked")
    public T remove() throws InterruptedException {
        //加鎖
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[removeIndex];
            if (++removeIndex == items.length)
                removeIndex = 0;
            --count;
            notFull.signal();
            return (T) x;
        } finally {
            lock.unlock();
        }
    }
}

6.1 Condition的實現分析

Condition是AQS的一個內部類,它主要依賴一個等待隊列來實現。

6.1.1.等待隊列

  等待隊列是一個FIFO的隊列,在隊列中的每個節點都包含了一個線程引用,該線程就是
在Condition對象上等待的線程,如果一個線程調用了Condition.await()方法,那麼該線程將會釋放鎖、構造成節點加入等待隊列並進入等待狀態。事實上,節點的定義複用了同步器中節點的定義,也就是說,同步隊列和等待隊列中節點類型都是同步器的靜態內部類
AbstractQueuedSynchronizer.Node。一個Condition包含一個等待隊列,Condition擁有首節點(firstWaiter)和尾節點(lastWaiter)。當前線程調用Condition.await()方法,將會以當前線程構造節點,並將節點從尾部加入等待隊列,等待隊列的基本結構如圖5-9所示。
在這裏插入圖片描述
  如圖所示,Condition擁有首尾節點的引用,而新增節點只需要將原有的尾節點nextWaiter指向它,並且更新尾節點即可。上述節點引用更新的過程並沒有使用CAS保證,原因在於調用await()方法的線程必定是獲取了鎖的線程,也就是說該過程是由鎖來保證線程安全的。

  在Object的監視器模型上,一個對象擁有一個同步隊列和等待隊列,而併發包中的
Lock(更確切地說是同步器)擁有一個同步隊列和多個等待隊列,其對應關係如圖5-10所示。
在這裏插入圖片描述

6.2 等待

  調用Condition的await()方法(或者以await開頭的方法),會使當前線程進入等待隊列並釋放鎖,同時線程狀態變爲等待狀態。當從await()方法返回時,當前線程一定獲取Condition相關聯的鎖。如果從隊列(同步隊列和等待隊列)的角度看await()方法,當調用await()方法時,相當於同步隊列的首節點(獲取了鎖的節點)移動到Condition的等待隊列中。

源碼分析:

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
    		//添加到等待隊列中去
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
    		//如果該節點不在同步隊列中,則阻塞
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

isOnSyncQueue(node)方法用來判斷是節點是否在同步隊列,只有condition被其他線程調用了signal()或signalAll()方法,節點纔有可能從等待隊列移到同步隊列。

addConditionWaiter()

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    //如果尾節點是空或者狀態爲cancelled,則刪除,找到正確的尾節點
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    //如果隊列爲空,則使用firstWaiter指向新節點
    if (t == null)
        firstWaiter = node;
    //否則尾節點的nextWaiter指向新結點
    else
        t.nextWaiter = node;
    //把新結點置爲尾節點
    lastWaiter = node;
    return node;
}

6.3 通知

  調用Condition的signal()方法,將會喚醒在等待隊列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移到同步隊列中。

源碼分析:

public final void signal() {
    //判斷當前線程是否獲取到了鎖
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

調用該方法的前置條件是當前線程必須獲取了鎖,可以看到signal()方法進行了
isHeldExclusively()檢查,也就是當前線程必須是獲取了鎖的線程。接着獲取等待隊列的首節
點,將其移動到同步隊列並使用LockSupport喚醒節點中的線程。

doSignal(first)

private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

transferForSignal(Node node)

final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
    //吧狀態從CONDITION改爲0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    //使用end方法將節點添加到同步隊列中
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        //喚醒線程
        LockSupport.unpark(node.thread);
    return true;
}

  通過調用同步器的enq(Node node)方法,等待隊列中的頭節點線程安全地移動到同步隊列。當節點移動到同步隊列後,當前線程再使用LockSupport喚醒該節點的線程。
被喚醒後的線程,將從await()方法中的while循環中退出(isOnSyncQueue(Node node)方法返回true,節點已經在同步隊列中),進而調用同步器的acquireQueued()方法加入到獲取同步狀態的競爭中。

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