Java多線程(八)之Semaphore、CountDownLatch、CyclicBarrier、Exchanger

一、引言


Semaphore               :一個計數信號量
CountDownLatch          :一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。 
CyclicBarrier           :一個同步輔助類,它允許一組線程互相等待,直到到達某個公共屏障點 
   Exchanger               :方便了兩個共同操作線程之間的雙向交換

二、Semaphore


Semaphore 是一個計數信號量。從概念上講,信號量維護了一個許可集。如有必要,在許可可用前會阻塞每一個 acquire(),然後再獲取該許可。每個 release() 添加一個許可,從而可能釋放一個正在阻塞的獲取者。但是,不使用實際的許可對象,Semaphore 只對可用許可的號碼進行計數,並採取相應的行動。

說白了,Semaphore是一個計數器,在計數器不爲0的時候對線程就放行,一旦達到0,那麼所有請求資源的新線程都會被阻塞,包括增加請求到許可的線程,也就是說Semaphore不是可重入的。每一次請求一個許可都會導致計數器減少1,同樣每次釋放一個許可都會導致計數器增加1,一旦達到了0,新的許可請求線程將被掛起。

緩存池整好使用此思想來實現的,比如鏈接池、對象池等。


計數信號可以用於限制有權對資源進行併發訪問的線程數。該方法對於實現資源池或限制 Web 爬蟲(Web crawler)中的輸出 socket 連接非常有用。

清單1 對象池

package xylz.study.concurrency.lock;

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

public class ObjectCache<T> {

    public interface ObjectFactory<T> {

        T makeObject();
    }

    class Node {

        T obj;

        Node next;
    }

    final int capacity;

    final ObjectFactory<T> factory;

    final Lock lock = new ReentrantLock();

    final Semaphore semaphore;

    private Node head;

    private Node tail;

    public ObjectCache(int capacity, ObjectFactory<T> factory) {
        this.capacity = capacity;
        this.factory = factory;
        this.semaphore = new Semaphore(this.capacity);
        this.head = null;
        this.tail = null;
    }

    public T getObject() throws InterruptedException {
        semaphore.acquire();
        return getNextObject();
    }

    private T getNextObject() {
        lock.lock();
        try {
            if (head == null) {
                return factory.makeObject();
            } else {
                Node ret = head;
                head = head.next;
                if (head == null) tail = null;
                ret.next = null;//help GC
                return ret.obj;
            }
        } finally {
            lock.unlock();
        }
    }

    private void returnObjectToPool(T t) {
        lock.lock();
        try {
            Node node = new Node();
            node.obj = t;
            if (tail == null) {
                head = tail = node;
            } else {
                tail.next = node;
                tail = node;
            }

        } finally {
            lock.unlock();
        }
    }

    public void returnObject(T t) {
        returnObjectToPool(t);
        semaphore.release();
    }

}


清單1描述了一個基於信號量Semaphore的對象池實現。此對象池最多支持capacity個對象,這在構造函數中傳入。對象池有一個基於FIFO的隊列,每次從對象池的頭結點開始取對象,如果頭結點爲空就直接構造一個新的對象返回。否則將頭結點對象取出,並且頭結點往後移動。特別要說明的如果對象的個數用完了,那麼新的線程將被阻塞,直到有對象被返回回來。返還對象時將對象加入FIFO的尾節點並且釋放一個空閒的信號量,表示對象池中增加一個可用對象。

實際上對象池、線程池的原理大致上就是這樣的,只不過真正的對象池、線程池要處理比較複雜的邏輯,所以實現起來還需要做很多的工作,例如超時機制,自動回收機制,對象的有效期等等問題。

這裏特別說明的是信號量只是在信號不夠的時候掛起線程,但是並不能保證信號量足夠的時候獲取對象和返還對象是線程安全的,所以在清單1中仍然需要鎖Lock來保證併發的正確性。

將信號量初始化爲 1,使得它在使用時最多隻有一個可用的許可,從而可用作一個相互排斥的鎖。這通常也稱爲二進制信號量,因爲它只能有兩種狀態:一個可用的許可,或零個可用的許可。按此方式使用時,二進制信號量具有某種屬性(與很多 Lock 實現不同),即可以由線程釋放“鎖”,而不是由所有者(因爲信號量沒有所有權的概念)。在某些專門的上下文(如死鎖恢復)中這會很有用。

上面這段話的意思是說當某個線程A持有信號量數爲1的信號量時,其它線程只能等待此線程釋放資源才能繼續,這時候持有信號量的線程A就相當於持有了“鎖”,其它線程的繼續就需要這把鎖,於是線程A的釋放才能決定其它線程的運行,相當於扮演了“鎖”的角色。


2.1 信號量的公平性


另外同公平鎖非公平鎖一樣,信號量也有公平性。如果一個信號量是公平的表示線程在獲取信號量時按FIFO的順序得到許可,也就是按照請求的順序得到釋放。這裏特別說明的是:所謂請求的順序是指在請求信號量而進入FIFO隊列的順序,有可能某個線程先請求信號而後進去請求隊列,那麼次線程獲取信號量的順序就會晚於其後請求但是先進入請求隊列的線程。這個在公平鎖和非公平鎖中談過很多。

 

除了acquire以外,Semaphore還有幾種類似的acquire方法,這些方法可以更好的處理中斷和超時或者異步等特性,可以參考JDK API。

按照同樣的學習原則,下面對主要的實現進行分析。Semaphore的acquire方法實際上訪問的是AQS的acquireSharedInterruptibly(arg)方法。這個可以參考http://blog.csdn.net/a511596982/article/details/8275624  一節。

所以Semaphore的await實現也是比較簡單的。與CountDownLatch不同的是,Semaphore區分公平信號和非公平信號。


清單2 公平信號獲取方法

protected int tryAcquireShared(int acquires) {
    Thread current = Thread.currentThread();
    for (;;) {
        Thread first = getFirstQueuedThread();
        if (first != null && first != current)
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}


清單3 非公平信號獲取方法

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}


對比清單2和清單3可以看到,公平信號和非公平信號在於第一次嘗試能否獲取信號時,公平信號量總是將當前線程進入AQS的CLH隊列進行排隊(因爲第一次嘗試時隊列的頭結點線程很有可能不是當前線程,當然不排除同一個線程第二次進入信號量),從而根據AQS的CLH隊列的順序FIFO依次獲取信號量;而對於非公平信號量,第一次立即嘗試能否拿到信號量,一旦信號量的剩餘數available大於請求數(acquires通常爲1),那麼線程就立即得到了釋放,而不需要進行AQS隊列進行排隊。只有remaining<0的時候(也就是信號量不夠的時候)纔會進入AQS隊列。

所以非公平信號量的吞吐量總是要比公平信號量的吞吐量要大,但是需要強調的是非公平信號量和非公平鎖一樣存在“飢渴死”的現象,也就是說活躍線程可能總是拿到信號量,而非活躍線程可能難以拿到信號量。而對於公平信號量由於總是靠請求的線程的順序來獲取信號量,所以不存在此問題。


三、CountDownLatch



3.1 閉鎖(Latch)


閉鎖(Latch):一種同步方法,可以延遲線程的進度直到線程到達某個終點狀態。通俗的講就是,一個閉鎖相當於一扇大門,在大門打開之前所有線程都被阻斷,一旦大門打開所有線程都將通過,但是一旦大門打開,所有線程都通過了,那麼這個閉鎖的狀態就失效了,門的狀態也就不能變了,只能是打開狀態。也就是說閉鎖的狀態是一次性的,它確保在閉鎖打開之前所有特定的活動都需要在閉鎖打開之後才能完成。


CountDownLatch是JDK 5+裏面閉鎖的一個實現,允許一個或者多個線程等待某個事件的發生。CountDownLatch有一個正數計數器,countDown方法對計數器做減操作,await方法等待計數器達到0。所有await的線程都會阻塞直到計數器爲0或者等待線程中斷或者超時。


CountDownLatch的API如下。

  • public void await() throws InterruptedException
  • public boolean await(long timeout, TimeUnit unit) throws InterruptedException
  • public void countDown()
  • public long getCount()

其中getCount()描述的是當前計數,通常用於調試目的。


清單4閉鎖的兩種常見的用法

import java.util.concurrent.CountDownLatch;

public class PerformanceTestTool {

    public long timecost(final int times, final Runnable task) throws InterruptedException {
        if (times <= 0) throw new IllegalArgumentException();
        final CountDownLatch startLatch = new CountDownLatch(1);
        final CountDownLatch overLatch = new CountDownLatch(times);
        for (int i = 0; i < times; i++) {
            new Thread(new Runnable() {
                public void run() {
                    try {
                        startLatch.await();
                        //
                        task.run();
                    } catch (InterruptedException ex) {
                        Thread.currentThread().interrupt();
                    } finally {
                        overLatch.countDown();
                    }
                }
            }).start();
        }
        //
        long start = System.nanoTime();
        startLatch.countDown();
        overLatch.await();
        return System.nanoTime() - start;
    }

}


在上面的例子中使用了兩個閉鎖,第一個閉鎖確保在所有線程開始執行任務前,所有準備工作都已經完成,一旦準備工作完成了就調用startLatch.countDown()打開閉鎖,所有線程開始執行。第二個閉鎖在於確保所有任務執行完成後主線程才能繼續進行,這樣保證了主線程等待所有任務線程執行完成後才能得到需要的結果。在第二個閉鎖當中,初始化了一個N次的計數器,每個任務執行完成後都會將計數器減一,所有任務完成後計數器就變爲了0,這樣主線程閉鎖overLatch拿到此信號後就可以繼續往下執行了。


根據前面的happend-before法則可以知道閉鎖有以下特性:

內存一致性效果:線程中調用 countDown() 之前的操作 happen-before 緊跟在從另一個線程中對應 await() 成功返回的操作。

在上面的例子中第二個閉鎖相當於把一個任務拆分成N份,每一份獨立完成任務,主線程等待所有任務完成後才能繼續執行。這個特性在後面的線程池框架中會用到,其實FutureTask就可以看成一個閉鎖。後面的章節還會具體分析FutureTask的。

 

同樣基於探索精神,仍然需要“窺探”下CountDownLatch裏面到底是如何實現await*countDown的。

首先,研究下await()方法。內部直接調用了AQSacquireSharedInterruptibly(1)

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}


前面一直提到的都是獨佔鎖(排它鎖、互斥鎖),現在就用到了另外一種鎖,共享鎖。

所謂共享鎖是說所有共享鎖的線程共享同一個資源,一旦任意一個線程拿到共享資源,那麼所有線程就都擁有的同一份資源。也就是通常情況下共享鎖只是一個標誌,所有線程都等待這個標識是否滿足,一旦滿足所有線程都被激活(相當於所有線程都拿到鎖一樣)。這裏的閉鎖CountDownLatch就是基於共享鎖的實現。



四、CyclicBarrier 


一個同步輔助類,它允許一組線程互相等待,直到到達某個公共屏障點 (common barrier point)。
在涉及一組固定大小的線程的程序中,這些線程必須不時地互相等待,此時 CyclicBarrier 很有用。因爲該 barrier 在釋放等待線程後可以重用,所以稱它爲循環 的 barrier


CyclicBarrier 支持一個可選的 Runnable 命令,在一組線程中的最後一個線程到達之後(但在釋放所有線程之前),
該命令只在每個屏障點運行一次。若在繼續所有參與線程之前更新共享狀態,此屏障操作 很有用。

清單5:下面是一個在並行分解設計中使用 barrier 的例子,很經典的旅行團例子:

import java.text.SimpleDateFormat;  
import java.util.Date;  
import java.util.concurrent.BrokenBarrierException;  
import java.util.concurrent.CyclicBarrier;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
public class TestCyclicBarrier {  
  // 徒步需要的時間: Shenzhen, Guangzhou, Shaoguan, Changsha, Wuhan  
  private static int[] timeWalk = { 5, 8, 15, 15, 10 };  
  // 自駕遊  
  private static int[] timeSelf = { 1, 3, 4, 4, 5 };  
  // 旅遊大巴  
  private static int[] timeBus = { 2, 4, 6, 6, 7 };  
    
  static String now() {  
     SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
     return sdf.format(new Date()) + ": ";  
  }  
  static class Tour implements Runnable {  
     private int[] times;  
     private CyclicBarrier barrier;  
     private String tourName;  
     public Tour(CyclicBarrier barrier, String tourName, int[] times) {  
       this.times = times;  
       this.tourName = tourName;  
       this.barrier = barrier;  
     }  
     public void run() {  
       try {  
         Thread.sleep(times[0] * 1000);  
         System.out.println(now() + tourName + " Reached Shenzhen");  
         barrier.await();  
         Thread.sleep(times[1] * 1000);  
         System.out.println(now() + tourName + " Reached Guangzhou");  
         barrier.await();  
         Thread.sleep(times[2] * 1000);  
         System.out.println(now() + tourName + " Reached Shaoguan");  
         barrier.await();  
         Thread.sleep(times[3] * 1000);  
         System.out.println(now() + tourName + " Reached Changsha");  
         barrier.await();  
         Thread.sleep(times[4] * 1000);  
         System.out.println(now() + tourName + " Reached Wuhan");  
         barrier.await();  
       } catch (InterruptedException e) {  
       } catch (BrokenBarrierException e) {  
       }  
     }  
  }  
  public static void main(String[] args) {  
     // 三個旅行團  
     CyclicBarrier barrier = new CyclicBarrier(3);  
     ExecutorService exec = Executors.newFixedThreadPool(3);  
     exec.submit(new Tour(barrier, "WalkTour", timeWalk));  
     exec.submit(new Tour(barrier, "SelfTour", timeSelf));  
//當我們把下面的這段代碼註釋後,會發現,程序阻塞了,無法繼續運行下去。  
     exec.submit(new Tour(barrier, "BusTour", timeBus));  
     exec.shutdown();  
  }  
}   


CyclicBarrier最重要的屬性就是參與者個數,另外最要方法是await()。當所有線程都調用了await()後,就表示這些線程都可以繼續執行,否則就會等待。


4.1 CyclicBarrier的特點


CyclicBarrier有以下幾個特點:

await()方法將掛起線程,直到同組的其它線程執行完畢才能繼續
await()方法返回線程執行完畢的索引,注意,索引時從任務數-1開始的,也就是第一個執行完成的任務索引爲parties-1,最後一個爲0,這個parties爲總任務數
CyclicBarrier 是可循環的,顯然名稱說明了這點。

另外CyclicBarrier除了以上特點外,還有以下幾個特點:

如果屏障操作不依賴於掛起的線程,那麼任何線程都可以執行屏障操作。

CyclicBarrier 的構造函數允許攜帶一個任務,這個任務將在0%屏障點執行,它將在await()==0後執行。
CyclicBarrier 如果在await時因爲中斷、失敗、超時等原因提前離開了屏障點,那麼任務組中的其他任務將立即被中斷,以InterruptedException異常離開線程。
所有await()之前的操作都將在屏障點之前運行,也就是CyclicBarrier 的內存一致性效果(happe)
 

CyclicBarrier 的所有API如下:

public CyclicBarrier(int parties) 創建一個新的 CyclicBarrier,它將在給定數量的參與者(線程)處於等待狀態時啓動,但它不會在啓動 barrier 時執行預定義的操作。
public CyclicBarrier(int parties, Runnable barrierAction) 創建一個新的 CyclicBarrier,它將在給定數量的參與者(線程)處於等待狀態時啓動,並在啓動 barrier 時執行給定的屏障操作,該操作由最後一個進入 barrier 的線程執行。
public int await() throws InterruptedException, BrokenBarrierException 在所有參與者都已經在此 barrier 上調用 await 方法之前,將一直等待。
public int await(long timeout,TimeUnit unit) throws InterruptedException, BrokenBarrierException,TimeoutException 在所有參與者都已經在此屏障上調用 await 方法之前將一直等待,或者超出了指定的等待時間。
public int getNumberWaiting() 返回當前在屏障處等待的參與者數目。此方法主要用於調試和斷言。
public int getParties() 返回要求啓動此 barrier 的參與者數目。
public boolean isBroken() 查詢此屏障是否處於損壞狀態。
public void reset() 將屏障重置爲其初始狀態。


五、Exchanger 


Exchanger 類方便了兩個共同操作線程之間的雙向交換;這樣,就像具有計數爲 2 的 CyclicBarrier,並且兩個線程在都到達屏障時可以“交換”一些狀態。(Exchanger 模式有時也稱爲聚集。)

Exchanger 通常用於一個線程填充緩衝(通過讀取 socket),而另一個線程清空緩衝(通過處理從 socket 收到的命令)的情況。當兩個線程在屏障處集合時,它們交換緩衝。下列代碼說明了這項技術:

清單6:

class FillAndEmpty {
   Exchanger<DataBuffer> exchanger = new Exchanger<DataBuffer>();
   DataBuffer initialEmptyBuffer = new DataBuffer();
   DataBuffer initialFullBuffer = new DataBuffer();

   class FillingLoop implements Runnable {
     public void run() {
       DataBuffer currentBuffer = initialEmptyBuffer;
       try {
         while (currentBuffer != null) {
           addToBuffer(currentBuffer);
           if (currentBuffer.full())
             currentBuffer = exchanger.exchange(currentBuffer);
         }
       } catch (InterruptedException ex) { ... handle ... }
     }
   }

   class EmptyingLoop implements Runnable {
     public void run() {
       DataBuffer currentBuffer = initialFullBuffer;
       try {
         while (currentBuffer != null) {
           takeFromBuffer(currentBuffer);
           if (currentBuffer.empty())
             currentBuffer = exchanger.exchange(currentBuffer);
         }
       } catch (InterruptedException ex) { ... handle ...}
     }
   }

   void start() {
     new Thread(new FillingLoop()).start();
     new Thread(new EmptyingLoop()).start();
   }
 }

JDK 6以後爲了支持多線程多對象同時Exchanger了就進行了改造(爲了支持更好的併發),採用ConcurrentHashMap的思想,將Stack分割成很多的片段(或者說插槽Slot),線程Id(Thread.getId())hash相同的落在同一個Slot上,這樣在默認32個Slot上就有很好的吞吐量。當然會根據機器CPU內核的數量有一定的優化,有興趣的可以去了解下Exchanger的源碼。

Exchanger實現的是一種數據分片的思想,這在大數據情況下將數據分成一定的片段並且多線程執行的情況下有一定的使用價值。


參考:
java的concurrent用法詳解http://blog.csdn.net/a511596982/article/details/8063742
Java多線程(七)之同步器基礎:AQS框架深入分析
http://blog.csdn.net/a511596982/article/details/8275624
JDK 5.0 中的併發
http://www.ibm.com/developerworks/cn/education/java/j-concur/section5.html
關於 java.util.concurrent 您不知道的 5 件事,第 2 部分
http://www.ibm.com/developerworks/cn/java/j-5things5.html
深入淺出 Java Concurrency (10): 鎖機制 part 5 閉鎖 (CountDownLatch)
http://www.blogjava.net/xylz/archive/2010/07/09/325612.html
深入淺出 Java Concurrency (11): 鎖機制 part 6 CyclicBarrier
http://www.blogjava.net/xylz/archive/2010/07/12/325913.html
深入淺出 Java Concurrency (12): 鎖機制 part 7 信號量(Semaphore)
http://www.blogjava.net/xylz/archive/2010/07/13/326021.html
深入淺出 Java Concurrency (26): 併發容器 part 11 Exchanger
http://www.blogjava.net/xylz/archive/2010/11/22/338733.html
Java線程學習筆記(十)CountDownLatch 和CyclicBarrier
http://www.iteye.com/topic/657295

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