一次聊天引發的思考--java併發包實戰

一次聊天,談到了死鎖的解決、可重入鎖等等,突然發現這些離自己很遠,只有一些讀書時的概念涌入腦海,但各自的應用場景怎麼都無法想出。痛定思痛,決定看看concurrent包裏涉及併發的類及各自的應用場景。

第一類:原子操作類的atomic包,裏面包含了

1)布爾類型的AtomicBoolean

2)整型AtomicInteger、AtomicIntegerArray、AtomicIntegerFieldUpdater

3)長整型AtomicLong、AtomicLongArray、AtomicLongFieldUpdater

4)引用型AtomicMarkableReference、AtomicReference、AtomicReferenceArray、AtomicReferenceFieldUpdater、AtomicStampedReference

5)累加器DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64

java.util.concurrent.atomic原子操作類包

這個包裏面提供了一組原子變量類。其基本的特性就是在多線程環境下,當有多個線程同時執行這些類的實例包含的方法時,具有排他性,即當某個線程進入方法,執行其中的指令時,不會被其他線程打斷,而別的線程就像自旋鎖一樣,一直等到該方法執行完成,才由JVM從等待隊列中選擇一個另一個線程進入,這只是一種邏輯上的理解。實際上是藉助硬件的相關指令來實現的,不會阻塞線程(或者說只是在硬件級別上阻塞了)。可以對基本數據、數組中的基本數據、對類中的基本數據進行操作。原子變量類相當於一種泛化的volatile變量,能夠支持原子的和有條件的讀-改-寫操作。

java.util.concurrent.atomic中的類可以分成4組:

標量類(Scalar):AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新器類:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
複合變量類:AtomicMarkableReference,AtomicStampedReference
第一組AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference這四種基本類型用來處理布爾,整數,長整數,對象四種數據,其內部實現不是簡單的使用synchronized,而是一個更爲高效的方式CAS (compare and swap) + volatile和native方法,從而避免了synchronized的高開銷,執行效率大爲提升。如AtomicInteger的實現片斷爲:

private static final Unsafe unsafe = Unsafe.getUnsafe();
private volatile int value;
public final int get() {
        return value;
}
public final void set(int newValue) {
        value = newValue;
}
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

構造函數(兩個構造函數)
默認的構造函數:初始化的數據分別是false,0,0,null
帶參構造函數:參數爲初始化的數據
set( )和get( )方法:可以原子地設定和獲取atomic的數據。類似於volatile,保證數據會在主存中設置或讀取
void set()和void lazySet():set設置爲給定值,直接修改原始值;lazySet延時設置變量值,這個等價於set()方法,但是由於字段是volatile類型的,因此次字段的修改會比普通字段(非volatile字段)有稍微的性能延時(儘管可以忽略),所以如果不是想立即讀取設置的新值,允許在“後臺”修改值,那麼此方法就很有用。
getAndSet( )方法
原子的將變量設定爲新數據,同時返回先前的舊數據
其本質是get( )操作,然後做set( )操作。儘管這2個操作都是atomic,但是他們合併在一起的時候,就不是atomic。在Java的源程序的級別上,如果不依賴synchronized的機制來完成這個工作,是不可能的。只有依靠native方法纔可以。

    public final int getAndSet(int newValue) {
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }

compareAndSet( ) 和weakCompareAndSet( )方法
這 兩個方法都是conditional modifier方法。這2個方法接受2個參數,一個是期望數據(expected),一個是新數據(new);如果atomic裏面的數據和期望數據一 致,則將新數據設定給atomic的數據,返回true,表明成功;否則就不設定,並返回false。JSR規範中說:以原子方式讀取和有條件地寫入變量但不 創建任何 happen-before 排序,因此不提供與除 weakCompareAndSet 目標外任何變量以前或後續讀取或寫入操作有關的任何保證。大意就是說調用weakCompareAndSet時並不能保證不存在happen- before的發生(也就是可能存在指令重排序導致此操作失敗)。但是從Java源碼來看,其實此方法並沒有實現JSR規範的要求,最後效果和 compareAndSet是等效的,都調用了unsafe.compareAndSwapInt()完成操作。

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final boolean weakCompareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

對於 AtomicInteger、AtomicLong還提供了一些特別的方法。
getAndIncrement( ):以原子方式將當前值加 1,相當於線程安全的i++操作。
incrementAndGet( ):以原子方式將當前值加 1, 相當於線程安全的++i操作。
getAndDecrement( ):以原子方式將當前值減 1, 相當於線程安全的i--操作。
decrementAndGet ( ):以原子方式將當前值減 1,相當於線程安全的--i操作。
addAndGet( ): 以原子方式將給定值與當前值相加, 實際上就是等於線程安全的i =i+delta操作。
getAndAdd( ):以原子方式將給定值與當前值相加, 相當於線程安全的t=i;i+=delta;return t;操作。
以實現一些加法,減法原子操作。(注意 --i、++i不是原子操作,其中包含有3個操作步驟:第一步,讀取i;第二步,加1或減1;第三步:寫回內存)

使用AtomicReference創建線程安全的堆棧

package thread;
import java.util.concurrent.atomic.AtomicReference;
public class ConcurrentStack<T> {
    private AtomicReference<Node<T>>    stacks    = new AtomicReference<Node<T>>();
    public T push(T e) {
        Node<T> oldNode, newNode;
        for (;;) { // 這裏的處理非常的特別,也是必須如此的。
            oldNode = stacks.get();
            newNode = new Node<T>(e, oldNode);
            if (stacks.compareAndSet(oldNode, newNode)) {
                return e;
            }
        }
    }    
    public T pop() {
        Node<T> oldNode, newNode;
        for (;;) {
            oldNode = stacks.get();
            newNode = oldNode.next;
            if (stacks.compareAndSet(oldNode, newNode)) {
                return oldNode.object;
            }
        }
    }    
    private static final class Node<T> {
        private T        object;        
        private Node<T>    next;        
        private Node(T object, Node<T> next) {
            this.object = object;
            this.next = next;
        }
    }    
}

雖然原子的標量類擴展了Number類,但並沒有擴展一些基本類型的包裝類,如Integer或Long,事實上他們也不能擴展:基本類型的包裝類是不可以修改的,而原子變量類是可以修改的。在原子變量類中沒有重新定義hashCode或equals方法,每個實例都是不同的,他們也不宜用做基於散列容器中的鍵值。

第二組AtomicIntegerArray,AtomicLongArray還有AtomicReferenceArray類進一步擴展了原子操作,對這些類型的數組提供了支持。這些類在爲其數組元素提供 volatile 訪問語義方面也引人注目,這對於普通數組來說是不受支持的。

他們內部並不是像AtomicInteger一樣維持一個valatile變量,而是全部由native方法實現,如下
AtomicIntegerArray的實現片斷:

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int scale = unsafe.arrayIndexScale(int[].class);
private final int[] array;
public final int get(int i) {
        return unsafe.getIntVolatile(array, rawIndex(i));
}
public final void set(int i, int newValue) {
        unsafe.putIntVolatile(array, rawIndex(i), newValue);
}

對Java技術,架構技術感興趣的同學,歡迎加羣,一起學習,相互討論。可以獲取免費的學習資料,羣號:614478470 點擊加入

第三組AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater基於反射的實用工具,可以對指定類的指定 volatile 字段進行原子更新。API非常簡單,但是也是有一些約束:

(1)字段必須是volatile類型的

(2)字段的描述類型(修飾符public/protected/default/private)是與調用者與操作對象字段的關係一致。也就是說 調用者能夠直接操作對象字段,那麼就可以反射進行原子操作。但是對於父類的字段,子類是不能直接操作的,儘管子類可以訪問父類的字段。

(3)只能是實例變量,不能是類變量,也就是說不能加static關鍵字。

(4)只能是可修改變量,不能使final變量,因爲final的語義就是不可修改。實際上final的語義和volatile是有衝突的,這兩個關鍵字不能同時存在。

(5)對於AtomicIntegerFieldUpdater 和AtomicLongFieldUpdater 只能修改int/long類型的字段,不能修改其包裝類型(Integer/Long)。如果要修改包裝類型就需要使用AtomicReferenceFieldUpdater 。

netty5.0中類ChannelOutboundBuffer統計發送的字節總數,由於使用volatile變量已經不能滿足,所以使用AtomicIntegerFieldUpdater 來實現的,看下面代碼:

//定義
    private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER =
            AtomicLongFieldUpdater.newUpdater(ChannelOutboundBuffer.class, "totalPendingSize");

    private volatile long totalPendingSize;

//使用
        long oldValue = totalPendingSize;
        long newWriteBufferSize = oldValue + size;
        while (!TOTAL_PENDING_SIZE_UPDATER.compareAndSet(this, oldValue, newWriteBufferSize)) {
            oldValue = totalPendingSize;
            newWriteBufferSize = oldValue + size;
        }

第二類:鎖的類包,裏面包含了

排他鎖:AbstractOwnableSynchronizer、AbstractQueuedLongSynchronizer、AbstractQueuedSynchronizer

讀寫鎖、可重入鎖:ReadWriteLock、ReentrantLock、Lock、ReentrantReadWriteLock(隱式包含讀鎖和寫鎖)、Condition、LockSupport

混合鎖:StampedLock

condition相似於對象的監控方法object#wait()、object#notify、object#notifyAll,但不同之處在於:通過和任意Lock的實現類聯合使用,Condition對每個對象提供了多個等待-設置功能。

此時Lock代替了synchronized方法和模塊,condition代替了對象的監控方法。

Condition通常也稱作Condition隊列或者condition變量,它提供了一種方法,使一個線程能夠暫停執行(wait方法),當別的線程的狀態condition爲true時可以激活此線程。由於不同線程共享的狀態信息必須受到保護,因此Condition具有一些鎖的形式。等待一個condition的關鍵屬性是自動釋放關聯的鎖並且暫停當前線程,類似於object.wait。

Conditon示例內部綁定了一個鎖,獲取一個特定鎖的實例的Condition實例可以通過lock#newCondition方法得到。

舉個condition的示例,先看一下生產者和消費者模式常規代碼:

/** 
         * 生產指定數量的產品 
         * 
         * @param neednum 
         */ 
        public synchronized void produce(int neednum) { 
                //測試是否需要生產 
                while (neednum + curnum > max_size) { 
                        System.out.println("要生產的產品數量" + neednum + "超過剩餘庫存量" + (max_size - curnum) + ",暫時不能執行生產任務!"); 
                        try { 
                                //當前的生產線程等待 
                                wait(); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
                //滿足生產條件,則進行生產,這裏簡單的更改當前庫存量 
                curnum += neednum; 
                System.out.println("已經生產了" + neednum + "個產品,現倉儲量爲" + curnum); 
                //喚醒在此對象監視器上等待的所有線程 
                notifyAll(); 
        } 

        /** 
         * 消費指定數量的產品 
         * 
         * @param neednum 
         */ 
        public synchronized void consume(int neednum) { 
                //測試是否可消費 
                while (curnum < neednum) { 
                        try { 
                                //當前的生產線程等待 
                                wait(); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
                //滿足消費條件,則進行消費,這裏簡單的更改當前庫存量 
                curnum -= neednum; 
                System.out.println("已經消費了" + neednum + "個產品,現倉儲量爲" + curnum); 
                //喚醒在此對象監視器上等待的所有線程 
                notifyAll(); 
        } 

我們希望保證生產者的produce線程和消費者的consume線程有不同的等待--設置,這樣,當buffer中的項目或者空間可用時,我們只需要每次只通知一個單線程就可以了。優化只要使用兩個Condition實例即可(略有改動):

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 produce(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 consume() 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();
      }
    }
  }

java.util.concurrent.ArrayBlockingQueue提供了上述功能,因此沒有必要實現這個示例的類。

Condition實現類可以提供和對象監控方法不同的行爲和語義,例如保證通知的順序,或者當執行通知時不要求保持一個鎖。若condition實現類提供了上述特定的語義,那麼實現類必須以文檔的形式聲明這些語義。

ReentrantReadWriteLock的讀鎖與寫鎖

讀鎖是排寫鎖操作的,讀鎖不排讀鎖操作,多個讀鎖可以併發不阻塞。在讀鎖獲取和讀鎖釋放之前,寫鎖並不能被任何線程獲取。多個讀鎖同時作用期間,試圖獲取寫鎖的線程都處於等待狀態,當最後一個讀鎖釋放後,試圖獲取寫鎖的線程纔有機會獲取寫鎖。
寫鎖是排寫鎖,排讀鎖操作的。當一個線程獲取到寫鎖之後,其他試圖獲取寫鎖和試圖獲取讀鎖的線程都處於等待狀態,直到寫鎖被釋放。
同時,寫鎖中是可以獲取讀鎖,但是讀鎖中是無法獲取寫鎖的。

下面的是java的ReentrantReadWriteLock官方示例,來解讀一下吧。

class CachedData {
    Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

void processCachedData() {
  rwl.readLock().lock();// @1
  if (!cacheValid) {
    // Must release read lock before acquiring write lock
    rwl.readLock().unlock(); // @3
    rwl.writeLock().lock(); // @2
    try {
      // Recheck state because another thread might have
      // acquired write lock and changed state before we did.
      if (!cacheValid) {
        data = ...
        cacheValid = true;
      }
      // Downgrade by acquiring read lock before releasing write lock
      rwl.readLock().lock(); //@4
    } finally {
      rwl.writeLock().unlock(); // Unlock write, still hold read @5
    }
  }

  try {
    use(data);
  } finally {
    rwl.readLock().unlock(); // 6
  }
}

當ABC三個線程同時進入到processcachedData()方法,同時都會得到讀鎖,然後獲取cachevalid,然後走到3位置釋放讀鎖,同時,假設A線程獲取到寫鎖,所以BC線程就無法獲取到寫鎖,這個時候進來的D線程就會停留在1位置而無法獲取讀鎖。A線程繼續往下走,判斷到cachevalid還是false,就會繼續走下去。爲什麼這個地方會還有一次判斷,上面註釋很清楚,A線程寫完之後,BC線程獲取到寫鎖,如果不再次進行判斷,就會寫入新的數據了,就不再是同步鎖了。所以這個地方有一個新的判斷。回到A線程,A線程繼續進行操作,到達4之後,獲取到讀鎖,這個地方api官方解釋就是,寫鎖要釋放的時候,必須先降級成讀鎖,這樣其他在等待寫鎖的比如BC,就不會獲取到寫鎖了。然後釋放寫鎖,這就是寫鎖的降級,釋放寫鎖之後,因爲還持有讀鎖,所以BC線程無法獲取到寫鎖,只有在A線程執行到6的時候,BC線程纔會拿到寫鎖,進行判斷,就會發現數據已經有了,釋放寫鎖,釋放讀鎖。

讀寫鎖能夠有效的在讀操作明顯大於寫操作的需求中完成高效率的運轉。

第三類:併發數據結構,包含了array、linkedList、set、map、list、queue等併發數據結構,包含如下:

阻塞數據結構:ArrayBlockingQueue、BlockingDeque、BlockingQueue、LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、

併發數據結構:ConcurrentHashMap、ConcurrentLinkedDeque、ConcurrentLinkedQueue、ConcurrentMap、ConcurrentNavigableMap、ConcurrentSkipListMap、ConcurrentSkipListSet

第四類:同步器 ,這部分主要是對線程集合的管理的實現,有Semaphore,CyclicBarrier, CountDownLatch,Exchanger等一些類。

對Java技術,架構技術感興趣的同學,歡迎加羣,一起學習,相互討論。可以獲取免費的學習資料,羣號:614478470 點擊加入

Semaphore
  類 java.util.concurrent.Semaphore 提供了一個計數信號量,從概念上講,信號量維護了一個許可集。如有必要,在許可可用前會阻塞每一個 acquire(),然後再獲取該許可。每個 release()添加一個許可,從而可能釋放一個正在阻塞的獲取者。但是,不使用實際的許可對象,Semaphore只對可用許可的號碼進行計數,並採取相應的行動。
  Semaphore 通常用於限制可以訪問某些資源(物理或邏輯的)的線程數目。 示例如下:

import java.util.*;import java.util.concurrent.*;

public class SemApp
{
    public static void main(String[] args)
{
        Runnable limitedCall = new Runnable() {
            final Random rand = new Random();
            final Semaphore available = new Semaphore(3);
            int count = 0;
            public void run()
{
                int time = rand.nextInt(15);
                int num = count++;

                try
                {
                    available.acquire();

                    System.out.println("Executing " + 
                        "long-running action for " + 
                        time + " seconds... #" + num);

                    Thread.sleep(time * 1000);

                    System.out.println("Done with #" + 
                        num + "!");

                    available.release();
                }
                catch (InterruptedException intEx)
                {
                    intEx.printStackTrace();
                }
            }
        };

        for (int i=0; i<10; i++)
            new Thread(limitedCall).start();
    }
}

即使本例中的 10 個線程都在運行(您可以對運行 SemApp 的 Java 進程執行 jstack 來驗證),但只有 3 個線程是活躍的。在一個信號計數器釋放之前,其他 7 個線程都處於空閒狀態。(實際上,Semaphore 類支持一次獲取和釋放多個 permit,但這不適用於本場景。)

CyclicBarrier
  java.util.concurrent.CyclicBarrier 一個同步輔助類,它允許 (common barrier point),在在涉及一組固定大小的線程的程序中,這些線程必須不時地互相等待,此時 CyclicBarrier 很有用。一組線程互相等待,直到到達某個公共屏障點。因爲該 barrier 在釋放等待線程後可以重用,所以稱它爲循環的 barrier。
  需要所有的子任務都完成時,才執行主任務,這個時候就可以選擇使用CyclicBarrier。賽跑時,等待所有人都準備好時,才起跑:

public class CyclicBarrierTest {

    public static void main(String[] args) throws IOException, InterruptedException {
        //如果將參數改爲4,但是下面只加入了3個選手,這永遠等待下去
        //Waits until all parties have invoked await on this barrier. 
        CyclicBarrier barrier = new CyclicBarrier(3);

        ExecutorService executor = Executors.newFixedThreadPool(3);
        executor.submit(new Thread(new Runner(barrier, "1號選手")));
        executor.submit(new Thread(new Runner(barrier, "2號選手")));
        executor.submit(new Thread(new Runner(barrier, "3號選手")));

        executor.shutdown();
    }
}

class Runner implements Runnable {
    // 一個同步輔助類,它允許一組線程互相等待,直到到達某個公共屏障點 (common barrier point)
    private CyclicBarrier barrier;

    private String name;

    public Runner(CyclicBarrier barrier, String name) {
        super();
        this.barrier = barrier;
        this.name = name;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000 * (new Random()).nextInt(8));
            System.out.println(name + " 準備好了...");
            // barrier的await方法,在所有參與者都已經在此 barrier 上調用 await 方法之前,將一直等待。
            barrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println(name + " 起跑!");
    }
}

輸出結果:
3號選手 準備好了...
2號選手 準備好了...
1號選手 準備好了...
1號選手 起跑!
2號選手 起跑!
3號選手 起跑!

CountDownLatch

類 java.util.concurrent.CountDownLatch 是一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。 用給定的數字作爲計數器初始化 CountDownLatch。一個線程調用 await()方法後,在當前計數到達零之前,會一直受阻塞。其他線程調用 countDown() 方法,會使計數器遞減,所以,計數器的值爲 0 後,會釋放所有等待的線程。其他後續的 await 調用都將立即返回。
這種現象只出現一次,因爲計數無法被重置。如果需要重置計數,請考慮使用 CyclicBarrier。

CountDownLatch 作爲一個通用同步工具,有很多用途。使用“ 1 ”初始化的
CountDownLatch 用作一個簡單的開/關鎖存器,或入口:在通過調用 countDown() 的線程
打開入口前,所有調用 await 的線程都一直在入口處等待。用 N 初始化的 CountDownLatch
可以使一個線程在 N 個線程完成某項操作之前一直等待,或者使其在某項操作完成 N 次之前一直等待。

此類持有所有空閒線程,直到滿足特定條件,這時它將會一次釋放所有這些線程。

清單 2. CountDownLatch:讓我們去賽馬吧!

import java.util.*;
import java.util.concurrent.*;

class Race
{
    private Random rand = new Random();

    private int distance = rand.nextInt(250);
    private CountDownLatch start;
    private CountDownLatch finish;

    private List<String> horses = new ArrayList<String>();

    public Race(String... names)
    {
        this.horses.addAll(Arrays.asList(names));
    }

    public void run()
        throws InterruptedException
    {
        System.out.println("And the horses are stepping up to the gate...");
        final CountDownLatch start = new CountDownLatch(1);
        final CountDownLatch finish = new CountDownLatch(horses.size());
        final List<String> places = 
            Collections.synchronizedList(new ArrayList<String>());

        for (final String h : horses)
        {
            new Thread(new Runnable() {
                public void run() {
                    try
                    {
                        System.out.println(h + 
                            " stepping up to the gate...");
                        start.await();

                        int traveled = 0;
                        while (traveled < distance)
                        {
                            // In a 0-2 second period of time....
                            Thread.sleep(rand.nextInt(3) * 1000);

                            // ... a horse travels 0-14 lengths
                            traveled += rand.nextInt(15);
                            System.out.println(h + 
                                " advanced to " + traveled + "!");
                        }
                        finish.countDown();
                        System.out.println(h + 
                            " crossed the finish!");
                        places.add(h);
                    }
                    catch (InterruptedException intEx)
                    {
                        System.out.println("ABORTING RACE!!!");
                        intEx.printStackTrace();
                    }
                }
            }).start();
        }

        System.out.println("And... they're off!");
        start.countDown();        

        finish.await();
        System.out.println("And we have our winners!");
        System.out.println(places.get(0) + " took the gold...");
        System.out.println(places.get(1) + " got the silver...");
        System.out.println("and " + places.get(2) + " took home the bronze.");
    }
}

public class CDLApp
{
    public static void main(String[] args)
        throws InterruptedException, java.io.IOException
    {
        System.out.println("Prepping...");

        Race r = new Race(
            "Beverly Takes a Bath",
            "RockerHorse",
            "Phineas",
            "Ferb",
            "Tin Cup",
            "I'm Faster Than a Monkey",
            "Glue Factory Reject"
            );

        System.out.println("It's a race of " + r.getDistance() + " lengths");

        System.out.println("Press Enter to run the race....");
        System.in.read();

        r.run();
    }
}

注意,CountDownLatch 有兩個用途:首先,它同時釋放所有線程,模擬馬賽的起點,但隨後會設置一個門閂模擬馬賽的終點。這樣,“主” 線程就可以輸出結果。 爲了讓馬賽有更多的輸出註釋,可以在賽場的 “轉彎處” 和 “半程” 點,比如賽馬跨過跑道的四分之一、二分之一和四分之三線時,添加 CountDownLatch。

Exchanger
  類 java.util.concurrent.Exchanger 提供了一個同步點,在這個同步點,一對線程可以交換
數據。每個線程通過 exchange()方法的入口提供數據給他的夥伴線程,並接收他的夥伴線程
提供的數據,並返回。

線程間可以用 Exchanger 來交換數據。當兩個線程通過 Exchanger 交互了對象,這個交換對於兩個線程來說都是安全的。

Future 和 FutureTask
  接口 public interface Future<V> 表示異步計算的結果。它提供了檢查計算是否完成的方法,
以等待計算的完成,並調用get()獲取計算的結果。
FutureTask 類是 Future 的一個實現, 並實現了Runnable ,所以可通過 Executor(線程池) 來執行。
也可傳遞給Thread對象執行。

如果在主線程中需要執行比較耗時的操作時,但又不想阻塞主線程時,可以把這些作業交給
Future 對象在後臺完成,當主線程將來需要時,就可以通過 Future 對象獲得後臺作業的計算結果或者執行狀態。

第五類:線程管理,

Callable 被執行的任務
Executor 執行任務
Future 異步提交任務的返回數據
QQ截圖20190320213341.png
Executor是總的接口,用來執行Runnable任務;
ExecutorService是Executor的擴展接口,主要擴展了執行Runnable或Callable任務的方式,及shutdown的方法;
ScheduledExecutorService是ExecutorService的擴展接口,主要擴展了可以用任務調度的形式(延遲或定期)執行Runnable或Callable任務;
AbstractExecutorService是ExecutorService接口的實現類,是抽象類,提供一些默認的執行Runnable或Callable任務的方法;
ThreadPoolExecutor是AbstractExecutorService的子類,是線程池的實現;
ScheduledThreadPoolExecutor是ThreadPoolExecutor的子類,實現ScheduledExecutorService接口,基於線程池模式的多任務調度,是Timer工具類的高性能版;
Callable與Future是Runnable的另外的形式,用來異步獲取任務執行結果;
最後,Executors是工具類,用於創建上述各種實例。

Q&A
Synchronization vs volatile

Synchronization supports mutual exclusion and visibility. In contrast, the volatile keyword only supports visibility.

對Java技術,架構技術感興趣的同學,歡迎加羣,一起學習,相互討論。可以獲取免費的學習資料,羣號:614478470 點擊加入

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