Java Multithreading - Synchronizer Semaphore, CountDownLatch, CyclicBarrier, Exchanger

Synchronizers are generally used with a set of thread objects, and it maintains a state, depending on its state, it lets a thread pass or forces a thread to wait .


1. Semaphore is a classic concurrency tool, usually used to limit the number of threads that can access certain resources (physical or logical).

class Pool {
   private static final int MAX_AVAILABLE = 100;//许可数
   private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);//Create a Semaphore with the given number of licenses and the given fairness settings.
   public Object getItem() throws InterruptedException {
     available.acquire();//Acquire a permission from this semaphore and block the thread until a permission is provided, otherwise the thread is interrupted.
     return getNextAvailableItem();
   }

   public void putItem(Object x) {
     if (markAsUnused(x))
       available.release();//Release a license and return it to the semaphore
   }

   // Not a particularly efficient data structure; just for demo

   protected Object[] items = ... whatever kinds of items being managed
   protected boolean[] used = new boolean[MAX_AVAILABLE];

   protected synchronized Object getNextAvailableItem() {//同步锁
     for (int i = 0; i < MAX_AVAILABLE; ++i) {
       if (!used[i]) {
          used[i] = true;
          return items[i];
       }
     }
     return null; // not reached
   }

   protected synchronized boolean markAsUnused(Object item) {
     for (int i = 0; i < MAX_AVAILABLE; ++i) {
       if (item == items[i]) {
          if (used[i]) {
            used[i] = false;
            return true;
          } else
            return false;
       }
     }
     return false;
   }

 }



[1]. Semaphore (controls the number of threads accessing resources)

    Conceptually, a semaphore maintains a set of permissions, blocking each acquire()thread until one is granted, and each release()adding a permission, potentially releasing a blocking acquirer. However, the actual license object is not used, only the number of available licenses is counted and the corresponding action is taken.


2.  CountDownLatch (Countdown Latch) is an extremely simple but extremely common utility used to block execution until a given number of signals, events or conditions are held.

Example usage:

Two classes are given below, where a set of worker threads use two countdown latches:

  1.      The first class is a start signal that prevents all workers from continuing until the driver is ready to continue.
  2.      The second class is a completion signal, which allows the driver to wait until all workers are finished.
class Driver { // ...
   void main() throws InterruptedException {
     CountDownLatch startSignal = new CountDownLatch(1);//Construct a CountDownLatch initialized with the given count.
     CountDownLatch doneSignal = new CountDownLatch(N);//Constructs a CountDownLatch initialized with the given count.

     for (int i = 0; i < N; ++i) // create and start threads
       new Thread(new Worker(startSignal, doneSignal)).start();

     doSomethingElse();            // don't let run yet
     startSignal.countDown(); // Decrement the count of the latch, if the count reaches zero, release all waiting threads.
     doSomethingElse();
     doneSignal.await(); // Causes the current thread to wait until the latch counts down to zero, unless the thread is interrupted.  
   }
 }

 class Worker implements Runnable {
   private final CountDownLatch startSignal;
   private final CountDownLatch doneSignal;
   Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
      this.startSignal = startSignal;
      this.doneSignal = doneSignal;
   }
   public void run() {
      try {
        startSignal.await();
        doWork();
        doneSignal.countDown();
} catch (InterruptedException ex) {} // return;
   }

   void doWork() { ... }
 }

Another typical usage is to divide a problem into N parts, describe each part with a Runnable that executes each part and counts down the latch, and then queues all Runnables to the Executor. When all subsections are complete, the coordinating thread can pass await. (A CyclicBarrier can be used instead when a thread must count down repeatedly this way.)

class Driver2 { // ...
   void main() throws InterruptedException {
     CountDownLatch doneSignal = new CountDownLatch(N);
     Executor and = ...

     for (int i = 0; i < N; ++i) // create and start threads
       e.execute(new WorkerRunnable(doneSignal, i));

     doneSignal.await();           // wait for all to finish
   }
 }

 class WorkerRunnable implements Runnable {
   private final CountDownLatch doneSignal;
   private final int i;
   WorkerRunnable(CountDownLatch doneSignal, int i) {
      this.doneSignal = doneSignal;
      this.i = i;
   }
   public void run() {
      try {
        doWork(i);
        doneSignal.countDown();
      } catch (InterruptedException ex) {} // return;
   }

   void doWork() { ... }
 }

3. CyclicBarrier is a resettable multiway synchronization point that is useful in certain parallel programming styles.

     A synchronization helper class that allows a group of threads to wait on each other until some common barrier point is reached. CyclicBarrier is useful in programs involving a set of fixed-size threads that must wait for each other from time to time. Because the barrier can be reused after releasing the waiting thread, it is called a cyclic barrier.

CyclicBarrier supports an optional Runnable command that runs only once at each barrier point after the last thread in a set of threads has arrived (but before all threads are released). This barrier operation is useful if the shared state is updated before continuing with all participating threads.

class Solver {
   final int N;
   final float[][] data;
   final CyclicBarrier barrier;//Create a new CyclicBarrier , which will start when a given number of participants (threads) are in a waiting state, but it will not perform predefined operations when starting the barrier.
   
   class Worker implements Runnable {
     int myRow;
     Worker(int row) { myRow = row; }
     public void run() {
       while (!done()) {
         processRow(myRow);

         try {
           barrier.await(); // Will wait until all participants have called await on this barrier.
         } catch (InterruptedException ex) {
return;
         } catch (BrokenBarrierException ex) {
return;
         }
       }
     }
   }

   public Solver(float[][] matrix) {
     data = matrix;
     N = matrix.length;
     barrier = new CyclicBarrier(N,
                                 new Runnable() {
                                   public void run() {
                                     mergeRows(...);
                                   }
                                 });
     for (int i = 0; i < N; ++i)
       new Thread(new Worker(i)).start();

     waitUntilDone ();
   }
 }

3. Exchanger allows two threads to exchange objects at the collection point, which is useful in multi-pipeline designs.

       A synchronization point for threads that can pair and swap elements in a pair. Each thread presents a method on the entry to the exchange method, matches up with the partner thread, and receives its partner's object on return. Exchanger might be seen as a bidirectional form of SynchronousQueue. Exchanger may be useful in applications such as genetic algorithms and pipeline design.


Usage example: Here is the highlighted class that Exchangeruses to swap buffers between threads, so when needed, the thread filling the buffer gets a newly emptied buffer and passes the filled buffer to emptied Buffer thread.

class FillAndEmpty {
   Exchanger<DataBuffer> exchanger = new Exchanger<DataBuffer>();
   DataBuffer initialEmptyBuffer = ... a made-up type
   DataBuffer initialFullBuffer = ...//Data buffer

   class FillingLoop implements Runnable {
     public void run() {
       DataBuffer currentBuffer = initialEmptyBuffer;
       try {
         while (currentBuffer != null) {
           addToBuffer(currentBuffer);
           if (currentBuffer.isFull())
             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.isEmpty())
             currentBuffer = exchanger.exchange(currentBuffer);//Wait for another thread to reach this exchange point (unless the current thread is interrupted), then transfer the given object to this thread and receive the thread's object.
         }
       } catch (InterruptedException ex) { ... handle ...}
     }
   }

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


Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325625989&siteId=291194637