併發編程系列之鎖基礎篇

前言

上節我們介紹了線程的相關知識,今天我們開始逛逛Java中鎖的相關旅途,今天我們先介紹基礎景點,主要講解下Java中的Lock接口和AQS,OK,讓我們開始今天的併發之旅吧。

 

景點一:Lock接口

什麼是Lock對象?

鎖是控制多個線程對共享資源進行訪問的工具。通常,鎖提供了對共享資源的獨佔訪問。一次只能有一個線程獲得鎖,對共享資源的所有訪問都需要首先獲得鎖。不過,某些鎖可能允許對共享資源併發訪問,如 ReadWriteLock 的讀取鎖。

synchronized 方法或語句的通過對每個對象使用隱式監視器鎖來實現同步,強制所有鎖獲取和釋放均要出現在一個結構塊中:當獲取了多個鎖時,它們必須以相反的順序釋放,即必須先獲取後釋放再獲取再釋放的順序,這種情況顯然沒有Lock接口的顯式實現加鎖解鎖方便,Lock接口擁有對鎖獲取和釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。

 

如何使用Lock接口?

Lock是一個接口,它定義了獲取鎖和釋放鎖的基本操作,主要有如下幾個方法:

Lock是個接口,不能直接使用,我們需要藉助Lock的具體實現來操作lock,主要的實現類有ReentrantLock, Condition, ReadWriteLock;以ReentrantLock爲例,我們看下面的僞代碼:

Lock lock = new ReentrantLock();
       // 獲取鎖
       lock.lock();
       try {
           // do some thing
       }finally {
           // 釋放鎖
           lock.unlock();
       }

這裏有二點要注意:

  • 獲取鎖的過程不要一定要寫在try外面,避免因爲獲取鎖失敗,導致lock鎖被無故釋放

  • 一定要在finally裏面釋放鎖,保證鎖獲取之後,最終能夠釋放鎖

 

Lock接口比synchronized優勢

lock和synchronized相比,主要優勢在於提供了下列幾種特有的性質:

  • 嘗試非阻塞的獲取鎖 tryLock():當前線程嘗試獲取鎖,如果沒有此時鎖沒有被其他線程獲取到,則當前線程獲取成功並持有鎖

  • 能夠被中斷的獲取鎖 lockInterruptibly():獲取到鎖的線程能夠被響應中斷,被中斷的線程將拋出異常,同時釋放所持有的鎖

  • 超時獲取鎖 tryLock(long, TimeUnit):在指定時間內獲取鎖,如果時間到了,還未獲取到鎖就立即返回

 

景點二:隊列同步器(AbstractQueuedSynchronizer)

什麼是AQS?

隊列同步器是用來構建鎖或者其他同步組件的基礎元素,主要是使用一個int成員變量來表示同步狀態,通過一個FIFO隊列來完成資源的獲取線程的排隊工作,可以理解爲,鎖是面向開發者的,隊列同步器是面向鎖的實現的;

 

如何使用AQS?

同步器的設計是基於模板的。使用者需要重寫同步器指定的方法,然後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法。而這些模板方法就是調用同步器使用者重寫的方法;

重寫同步器時,需要使用同步器提供的三個方法來訪問或者修改同步狀態:

  • getState():獲取當前同步狀態

  • setState(int  newState):設置當前同步狀態

  • compareAndSetState(int expect,int update):使用CAS設置當前狀態,該方法能保證狀態設置的原子性

 

可重寫的方法有如下五個:

  • tryAcquire(int arg) :獨佔式獲取同步狀態,該方法需要查詢當前狀態並判斷同步狀態是否符合預期,然後再進行CAS設置同步狀態

  • tryRelease(int arg) :獨佔式釋放同步狀態,等待獲取同步狀態的線程將有機會獲取同步狀態

  • tryAcquireShared(int  arg) :共享式獲取同步狀態,返回大於等於0的值,表示獲取成功,否則失敗

  • tryReleaseShared(int arg): 共享式釋放同步狀態

  • isHeldExclusively() :當前同步器是否在獨佔模式下被線程佔用,一般該方法表示是否被前當線程多獨佔

 

同步器提供的模板方法:

  • acquire(int arg) :獨佔式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,將會進入同步隊列等待,該方法將會調用重寫的tryAcquire(int arg) 方法

public final void acquire(int arg) {
       if (!tryAcquire(arg) &&
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
   }
  • acquireInterruptibly(int arg): 與acquire(int arg) 相同,但是該方法響應中斷,當前線程未獲取到同步狀態而進入同步隊列中,如果當前被中斷,則該方法會拋出InterruptedException並返回

public final void acquireInterruptibly(int arg)
       throws InterruptedException {
   if (Thread.interrupted())
       throw new InterruptedException();
   if (!tryAcquire(arg))
       doAcquireInterruptibly(arg);
}
  • tryAcquireNanos(int arg,long nanos):在acquireInterruptibly基礎上增加了超時限制,如果當前線程在超時時間內沒有獲取到同步狀態,那麼將會返回false,獲取到了返回true

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
           throws InterruptedException {
       if (Thread.interrupted())
           throw new InterruptedException();
       return tryAcquire(arg) ||
           doAcquireNanos(arg, nanosTimeout);
   }
  • 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;
   }
  • acquireShared(int arg): 共享式獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待。與獨佔式的不同是同一時刻可以有多個線程獲取到同步狀態

public final void acquireShared(int arg) {
       if (tryAcquireShared(arg) < 0)
           doAcquireShared(arg);
   }
  • acquireSharedInterruptibly(int arg) :與acquire(int arg) 相同,但是該方法響應中斷,當前線程未獲取到同步狀態而進入同步隊列中,如果當前被中斷,則該方法會拋出InterruptedException並返回

public final void acquireSharedInterruptibly(int arg)
           throws InterruptedException {
       if (Thread.interrupted())
           throw new InterruptedException();
       if (tryAcquireShared(arg) < 0)
           doAcquireSharedInterruptibly(arg);
   }
  • (int arg,long nanos):在acquireSharedInterruptibly基礎上增加了超時限制,如果當前線程在超時時間內沒有獲取到同步狀態,那麼將會返回false,獲取到了返回true

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
          throws InterruptedException {
      if (Thread.interrupted())
          throw new InterruptedException();
      return tryAcquireShared(arg) >= 0 ||
          doAcquireSharedNanos(arg, nanosTimeout);
  }
  • releaseShared(int arg) :共享式釋放同步狀態

public final boolean releaseShared(int arg) {
       if (tryReleaseShared(arg)) {
           doReleaseShared();
           return true;
       }
       return false;
   }
  • getQueuedThreads(): 獲取等待在同步隊列上的線程集合

public final Collection<Thread> getQueuedThreads() {
       ArrayList<Thread> list = new ArrayList<Thread>();
       for (Node p = tail; p != null; p = p.prev) {
           Thread t = p.thread;
           if (t != null)
               list.add(t);
       }
       return list;
   }

同步器提供的上述方法主要分爲三類:

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

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

  • 查詢同步隊列中的等待線程情況

 

隊列同步器的實現

同步隊列

同步器依賴內部的同步隊列(FIFO)來完成同步狀態的管理,過程如下:當前線程獲取同步狀態失敗時,同步器就會將當前線程以及等待狀態的信息構成一個節點並加入到同步隊列中,同時阻塞當前線程,當同步狀態釋放時,會將首節點的線程喚醒,再次嘗試獲取同步狀態:

對於Node節點我們來了解下,看下面源碼對Node的定義:

static final class Node {
       static final Node SHARED = new Node();
       static final Node EXCLUSIVE = null;

       static final int CANCELLED =  1;  
       static final int SIGNAL    = -1;
       static final int CONDITION = -2;
       static final int PROPAGATE = -3;
       /**
        *   等待狀態值,分爲以下狀態值:
        *
        *   SIGNAL:     值爲-1 ,後續節點處於等待狀態,而當前節點的線程如果
        *               釋放了同步狀態或者取消等待,節點進入該狀態不會變化          
        *   CANCELLED:  值爲 1,由於在同步隊列中等待的線程等待超時或者被中斷
        *               需要從同步隊列中取消等待,節點進入該狀態將不會變化                    
        *   CONDITION:  值爲-2,節點在等待隊列中,節點線程等待在Condition上,
        *               當其他線程對Condition調用了signal()方法後,該節點將會
        *               從等待隊裏中轉移到同步隊列中,加入對同步狀態的獲取中    
        *   PROPAGATE:  值爲-3,表示下一次共享式同步狀態獲取將會無條件地被傳播下去
        *            
        *   0:          初始化狀態
        */  
       volatile int waitStatus;
       // 前驅節點,當節點加入同步隊列時被設置(尾部添加)
       volatile Node prev;
       // 後繼節點
       volatile Node next;
       // 獲取同步狀態的線程
       volatile Thread thread;
       // 等待隊列中的後繼節點。如果當前節點是共享的,那麼這個字段是一個shared常量,
       // 也就是說節點類型(獨佔或共享)和等待隊列中個後繼節點共用同一個字段
       Node nextWaiter;

       final boolean isShared() {
           return nextWaiter == SHARED;
       }

       final Node predecessor() throws NullPointerException {
           Node p = prev;
           if (p == null)
               throw new NullPointerException();
           else
               return p;
       }

       Node() {    // Used to establish initial head or SHARED marker
       }

       Node(Thread thread, Node mode) {     // Used by addWaiter
           this.nextWaiter = mode;
           this.thread = thread;
       }

       Node(Thread thread, int waitStatus) { // Used by Condition
           this.waitStatus = waitStatus;
           this.thread = thread;
       }
   }

我們再來介紹下同步隊列的結構:同步器擁有首節點和尾節點,沒有成功獲取同步狀態的線程會成爲節點加入該隊列的尾部,如下圖:

節點加入尾節點:如果一個線程沒有獲得同步隊列,那麼包裝它的節點將被加入到隊尾,顯然這個過程應該是線程安全的。因此同步器提供了一個基於CAS的設置尾節點的方法:compareAndSetTail(Node expect,Node update),它需要傳遞一個它認爲的尾節點和當前節點,只有設置成功,當前節點才被加入隊尾,並真正與上個尾節點建立連接,過程如下:

首節點設置:首節點是獲取同步狀態成功的節點,首節點線程在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點,並且與之前的首節點斷開聯繫,過程如下圖:

 

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

獨佔式同步狀態的獲取:獨佔式獲取同步狀態的方法是acquried(int arg),該方法對中斷不敏感,也就是由於線程獲取同步狀態失敗後進入同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列移除,其源代碼如下:

// 同步狀態獲取、節點構造、加入同步隊列以及在同步隊列中自旋等操作
// 1.調用自定義同步器的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態
// 2.如果獲取失敗,就構造一個獨佔式(Node.EXCLUSIVE)的同步節點,並通過addWaiter方法加入到同步節點的尾部
// 3.最後調用acquiredQueued方法,該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程中斷來實現
public final void acquire(int arg) {
       if (!tryAcquire(arg) &&
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
   }

addWaiter方法如下:

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

enq方法如下:利用死循環不斷地嘗試設置尾節點

private Node enq(final Node node) {
   // 死循環

       for (;;) {
           Node t = tail;          

           // 如果尾節點爲空 則進行初始化

           if (t == null) {
               if (compareAndSetHead(new Node()))
                   tail = head;
           } else {
               node.prev = t;
               // 利用CAS設置尾節點
               if (compareAndSetTail(t, node)) {
                   t.next = node;
                   return t;
               }
           }
       }
   }

 

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

節點自旋過程如下圖:

獨佔式同步狀態的釋放:最後隊列調用同步器的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;
   }

上述方法執行完,會喚醒頭節點的後繼節點線程,unparkSuccessor通過使用LockSupport在喚醒處於等待狀態的線程:

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

 

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

共享式獲取與獨佔式獲取的區別就是同一時刻是否可以多個線程同時獲取到同步狀態。以文件的讀寫來說,讀操作的話同一時刻可以有很多線程在進行並阻塞寫操作,但是寫操作只能有一個線程在寫並阻塞所有讀操作。

共享式同步狀態的獲取:調用同步器的acquireShare(int arg) 方法可以共享式地獲取同步狀態,源碼如下:

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

doAcquireShared方法如下:在這個方法中,同步器調用tryAcquireShared方法嘗試獲取同步狀態,tryAcquireShared返回值是一個int類型,當返回值大於0時,表示能夠獲取到同步狀態。因此同步隊列裏的節點結束自旋狀態的條件就是tryAcquireShared返回值大於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);
       }
   }

 

共享式同步狀態的釋放:該方法在釋放同步狀態後,將會喚醒後續處於等待狀態的節點,和獨佔式最大的區別是tryReleaseShared方法必須確保是同步狀態線程安全釋放,因爲釋放同步狀態的操作會同時來自多個線程,所以一般使用CAS來保證線程安全,源碼如下:

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

 

private void doReleaseShared() {
       for (;;) {
           Node h = head;
           if (h != null && h != tail) {
               int ws = h.waitStatus;
               if (ws == Node.SIGNAL) {
                   if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                       continue;            // loop to recheck cases
                   unparkSuccessor(h);
               }
               else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                   continue;                // loop on failed CAS
           }
           if (h == head)                   // loop if head changed
               break;
       }
   }

 

獨佔式超時獲取同步狀態

通過調用同步器的doAcquireNanos方法可以超時獲取同步狀態,也就是說可以在給定時間獲取同步狀態,如果獲取到了則返回true,否則返回false,源碼如下:

private boolean doAcquireNanos(int arg, long nanosTimeout)
       throws InterruptedException {
       long lastTime = System.nanoTime();
       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;
               }
               if (nanosTimeout <= 0)
                   return false;
               if (shouldParkAfterFailedAcquire(p, node) &&
                   nanosTimeout > spinForTimeoutThreshold)
                   LockSupport.parkNanos(this, nanosTimeout);
                   
               // 先計算最後期限deadline,deadline=系統當前時間+nanosTimeout(超時時間),
                                         // 當線程喚醒後用deadline-系統當前時間,如果小於0,那麼超時,
          // 否則還需要睡眠nanosTimeout = deadline - 系統當前時間

               long now = System.nanoTime();
               nanosTimeout -= now - lastTime;
               lastTime = now;
               if (Thread.interrupted())
                   throw new InterruptedException();
           }
       } finally {
           if (failed)
               cancelAcquire(node);
       }
   }

     

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