從併發程式設計到分散式系統-如何處理海量資料(上)

Marki發表於2018-05-27

面試網際網路公司不得不說的高併發!
在這裡想寫寫自己在學習併發處理的學習思路,也會聊聊自己遇到的那些坑,以此為記,希望鞭策自己不斷學習、永不放棄!

具體筆者認為大體可分為分兩部分:
第一部分:Java多執行緒程式設計。
第二部分:高併發的解決思路。
第三部分:分散式架構中redis、zookeeper分散式鎖的應用。

本文著重講解第一塊。
1、Java記憶體模型與執行緒。

併發程式設計主要討論以下幾點:多個執行緒操作相同資源,保證執行緒安全,合理使用資源。

通常我們可以將物理計算機中出現的併發問題類比到JVM中的併發。

物理計算機處理器、快取記憶體、主記憶體間互動關係如圖:

從併發程式設計到分散式系統-如何處理海量資料(上)

處理器和記憶體的執行速度存在幾個數量級別的差距,因此為解決此矛盾引入了快取記憶體這一概念。當多個處理器的執行任務都涉及到同一塊主記憶體區域時,將可能導致各自快取資料的不一致問題,為解決一致性問題,需要各個處理器訪問快取時都遵循一些協議,在讀寫時要根據協議來進行操作。(MSI、MESI、MOSI、Synapse、Firefly及Dragon Protocol等)

處理器為提高效能,會對輸入程式碼亂序執行(Out-Of-Order Execution) 優化。

類比Java記憶體模型,執行緒、主記憶體、工作記憶體互動關係如圖:

從併發程式設計到分散式系統-如何處理海量資料(上)
JMM定義了程式中各個變數訪問規則,即在虛擬機器中將記憶體取出和儲存的底層細節。

從併發程式設計到分散式系統-如何處理海量資料(上)
執行緒A如果要跟執行緒B要通訊的話,必須經歷以下兩個步驟: 1)執行緒A把本地記憶體A中更新過的共享變數的值重新整理到主記憶體中。 2)執行緒B去主記憶體中讀取A更新過的共享變數的值。

執行緒的工作記憶體中儲存了該執行緒使用到變數的主記憶體副本拷貝(也可理解為此執行緒的私有拷貝),執行緒對變數的操作(讀取、賦值等)都在工作記憶體中進行,而不能直接讀寫主記憶體中變數。不同執行緒之間的通訊業需要通過主記憶體來完成。 主記憶體對應Java堆中物件例項資料部分,而工作記憶體則對應虛擬機器棧中部分割槽域。

在此還有非常重要的點需要提及!
指令重排序
執行程式時,為提高效能,編譯器和處理器常常會對指令做出重排序。分三種:
1)編譯器優化的重排序。
2)指令並行重排序。
3)記憶體系統重排序。
JMM的編譯器會禁止特定型別的編譯器重排序,對於處理器重排序(後兩者),則要求Java編譯器在生成指令序列時,插入特定型別的記憶體屏障指令,通過記憶體屏障指令來禁止特定型別的處理器重排序。

記憶體之間的互動操作

從併發程式設計到分散式系統-如何處理海量資料(上)
JMM中定義了8種操作來來描述工作記憶體與主記憶體之間的實現細節。

  • lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔狀態。
  • unlock(解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
  • read(讀取):作用於主記憶體的變數,它把一個變數從主記憶體傳輸到執行緒工作記憶體中,以便後邊的load操作。
  • load(載入):作用於主記憶體的變數,它把read操作從主記憶體中得到的變數值放到工作記憶體副本中。
  • use(使用):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
  • assign(賦值):作用於工作記憶體的變數,它把從執行引擎接收到的值賦給工作記憶體,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行此操作。
  • store(儲存):作用於工作記憶體的變數,它把工作記憶體的變數的值傳送到主記憶體中,以便以後的write操作使用。
  • write(寫入):作用於主記憶體的變數,它把store操縱從工作記憶體中得到的變數值放入到主記憶體的變數中。

JMM規定了執行上述八種操作時必須滿足的規則(與happens-before原則是等效的,即先行發生原則):

  • 不允許read和load、store和write操作之一單獨出現
  • 不允許一個執行緒丟棄它的最近assign的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中。
  • 不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。
  • 一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。即就是對一個變數實施use和store操作之前,必須先執行過了assign和load操作。
  • 一個變數在同一時刻只允許一條執行緒對其進行lock操作,lock和unlock必須成對出現。
  • 如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值。
  • 如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他執行緒鎖定的變數。
  • 對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作)。

相關JVM補充內容請查閱:JVM-攻城掠地

2、測試工具
PostMan、Apache Bench、JMeter、LoadRunner

3、執行緒安全性
原子性:提供了互斥訪問,同一時刻只能由一個執行緒來對它進行操作。
可見性:一個執行緒對主記憶體的修改可以及時被其他執行緒觀察到。
有序性:一個執行緒觀察其它執行緒中指令執行順序,由於指令重排序的存在,觀察的結果一般為雜亂無章的。 Java程式的天然有序性可以總結為:如果本執行緒內觀察,所有的操作都是有序的;如果在一個執行緒觀察另一個執行緒,所有的操作都是無須的。前者指的是執行緒內的序列語義,後者指的是指令重排序和工作記憶體和主記憶體同步延遲現象。

原子性-Atomic包

  • AtomicXXX:CAS、Unsafe.compareAndSwapInt

從併發程式設計到分散式系統-如何處理海量資料(上)
通過CAS來保證原子性,即Compare And Swap 比較交換:
CAS利用處理器提供的CMPXCHG指令實現,自旋CAS實現的基本思路就是迴圈進行CAS直到成功為止。 比較記憶體的值與預期的值,若相同則修改預期的值。

CAS雖然可以進行高效的進行原子操作,但是CAS仍在存在三大問題。

  • ABA問題。 在Java1.5開始,JDK的Atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。 大部分情況下ABA問題並不影響程式併發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更加高效。
  • 迴圈時間長開銷大,
  • 以及只能保證一個共享變數進行的原子操作。

測試:

public class AtomicExample1 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時併發執行的執行緒數
    public static int threadTotal = 200;

    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    private static void add() {
        count.incrementAndGet();
        // count.getAndIncrement();
    }
}
複製程式碼

AtomicInteger
原始碼實現

  public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
  public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
    //當前的指為var2,底層穿過來的值var5 如果當前的值與底層傳過來的值一樣的話,則將其更新問var5+var4
複製程式碼

AtomicLong與LongAdder

  • Java記憶體模型要求lock、unlock、read、load、assign、use、store、write這8個操作都是具有原子性,但是對於64位的資料型別(long、double),允許虛擬機器將沒有被volatile修飾的64位資料的讀寫操作劃分為兩次32位的操作來進行,即允許虛擬機器實現選擇可以不保證64位資料型別的load、store、read和write這四個原子操作,但是可以視為原子性操作。
  • AtomicLong CAS中如果併發量大,則會不斷進行迴圈呼叫,效率會比較低。
  • LongAdder實現熱點資料的分離,更快,如果有併發更新可能會出現誤差。 底層用陣列實現,其結果為陣列的求和累加。
    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

    /**
     * Equivalent to {@code add(1)}.
     */
    public void increment() {
        add(1L);
    }
複製程式碼

AtomicBoolean

  • 希望某件事情只執行一次。
  public final boolean compareAndSet(boolean expect, boolean update) {
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }
複製程式碼

AtomicReference

 public final V getAndSet(V newValue) {
        return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
    }
 public final Object getAndSetObject(Object var1, long var2, Object var4) {
        Object var5;
        do {
            var5 = this.getObjectVolatile(var1, var2);
        } while(!this.compareAndSwapObject(var1, var2, var5, var4));

        return var5;
    }
複製程式碼

AtomicIntegerFieldUpdater

  • 以原子性更新類中某一個屬性,這屬性需要用volatile進行修飾。
public class AtomicExample5 {

    private static AtomicIntegerFieldUpdater<AtomicExample5> updater =
            AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");

    @Getter
    public volatile int count = 100;

    public static void main(String[] args) {

        AtomicExample5 example5 = new AtomicExample5();

        if (updater.compareAndSet(example5, 100, 120)) {
            log.info("update success 1, {}", example5.getCount());
        }

        if (updater.compareAndSet(example5, 100, 120)) {
            log.info("update success 2, {}", example5.getCount());
        } else {
            log.info("update failed, {}", example5.getCount());
        }
    }
}
複製程式碼

AtomicStampedReference

  • 作用是首先檢查當前引用是否等於預期引用,並且檢查當前標誌是否等於預期標誌,如果全部相等,則以原子的方式將該引用和該標誌的值設定為給定的更新值。
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
複製程式碼

AtomicLongArray 維護陣列

原子性-鎖及對比

  • synchronized:依賴JVM,不可中斷鎖,適合鎖競爭不激烈情況下(併發相對較小),程式碼的可讀性好。
  • Lock:依賴特殊的CPU指令,程式碼實現,ReentrantLock。可中斷鎖,多樣化同步,競爭激烈的時候能維持常態。
  • Atomic:競爭激烈的時候能維持常態,比Lock效能更好,只能同步一個值。

執行緒安全-可見性
導致共享變數線上程間不可見的原因
1)執行緒交叉執行。
2)重排序結合執行緒交叉執行。
3)共享變數更新後的值沒有在工作記憶體與主記憶體及時更新。
JMM關於synchronizd的兩條規定:

  • 執行緒解鎖前,必須把共享變數的最新值重新整理到主記憶體。
  • 執行緒加鎖時,將清空工作記憶體中共享變數的值,從而使用共享變數時需要從主記憶體中讀取最新的值。

volatile-可見性 通過加入記憶體屏障和禁止重排序優化實現。

  • 對volatile變數寫操作時,會在寫操作後加入一條store屏障指令,將本地記憶體共享變數的值重新整理到主記憶體。
  • 對volatile變數讀操作時,會在讀操作前加入一條load屏障指令,從主記憶體中讀取共享變數。

必須符合以下場景才可使用:

  • 運算結果並不依賴變數當前值,或者能夠確保只有單一執行緒修改變數的值。
  • 變數不需要與其他狀態變數共同參與不變約束。
    原因:volatile變數在各個執行緒工作記憶體中不存在一致性問題,但是Java裡面的運算並非原子性操作,導致volatile變數運算在併發下一樣是不安全的。(可以通過反編譯來驗證)
private static void add() {
        count++;
        // 1、count 取出當前記憶體中的值
        // 2、+1
        // 3、count 寫回主存
        //即:兩個執行緒同時執行+1寫回主存就出現問題。
    }
複製程式碼

volatile通常用來作為狀態標記量

volatile boolean inited = false;
//執行緒1:
context = loadContext();
inited = true;
//執行緒2;
while (!inited){
    sleep();
}
doSomethingWithConfig(context);
複製程式碼

4、安全釋出物件
釋出物件:使一個物件能夠被當前範圍之外程式碼所使用。
物件逸出:一種錯誤的釋出。當一個物件還沒有構造完成,就能被其它執行緒所見。
安全釋出物件

  • 在靜態初始化函式中初始化一個物件的引用。
  • 將物件的引用儲存到volatile型別域或者AtomicReference物件中。
  • 物件引用儲存到某個正確構造物件final型別域中。
  • 將物件的引用儲存到一個由鎖保護的域中。

對此單例模式是個很好的學習例子:

public class SingletonExample4 {

    // 私有建構函式
    private SingletonExample4() {

    }

    // 1、memory = allocate() 分配物件的記憶體空間
    // 2、ctorInstance() 初始化物件
    // 3、instance = memory 設定instance指向剛分配的記憶體

    // JVM和cpu優化,發生了指令重排

    // 1、memory = allocate() 分配物件的記憶體空間
    // 3、instance = memory 設定instance指向剛分配的記憶體
    // 2、ctorInstance() 初始化物件

    // 單例物件
    private volatile static SingletonExample4 instance = null;

    // 靜態的工廠方法
    public static SingletonExample4 getInstance() {
        if (instance == null) { // 雙重檢測機制        // B
            synchronized (SingletonExample4.class) { // 同步鎖
                if (instance == null) {
                    instance = new SingletonExample4(); // A - 3
                }
            }
        }
        return instance;
    }
}
複製程式碼

通過列舉實現單例模式

/**
 * 列舉模式:最安全
 */
@ThreadSafe
@Recommend
public class SingletonExample7 {

    // 私有建構函式
    private SingletonExample7() {

    }

    public static SingletonExample7 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;

        private SingletonExample7 singleton;

        // JVM保證這個方法絕對只呼叫一次
        Singleton() {
            singleton = new SingletonExample7();
        }

        public SingletonExample7 getInstance() {
            return singleton;
        }
    }
}
複製程式碼

5、執行緒安全策略

1) 不可變物件
滿足條件:

  • 物件建立以後其狀態就不能修改。
  • 物件對所有域都是final型別。
  • 物件是正確建立的。(物件在建立期間,this沒有逸出)
  • Collections.unmodifiableXXX:Collection、List、Set、Map……
  • Guava:ImmutableXXX:Collection、List、Set、Map……

從併發程式設計到分散式系統-如何處理海量資料(上)
2) 執行緒封閉

  • Ad-hoc執行緒封閉:程式控制實現,最糟糕,忽略。
  • 堆疊封閉:區域性變數,無併發問題。
  • ThreadLocal執行緒封閉:特別好的封閉方法。(實現許可權管理)

3) 執行緒不安全寫法

  • StringBuilder -> StringBuffer
  • SimpleDateFormat -> JodaTime(推薦)
  • ArrayList、HashSet、HashMap等Collections
  • 先檢查再執行:if(condition(a)){handle(a);} ->非原子操作

4) 同步容器

  • ArrayList -->Vector,Stack
  • HashMap -->HashTable (key、value不能為null)
  • Collections.synchronizedXXX(List、Set、Map)
    注意:同步容器在某些場合並不一定可以做到執行緒安全。
    從併發程式設計到分散式系統-如何處理海量資料(上)
    5) 執行緒安全-併發容器-J.U.C

從併發程式設計到分散式系統-如何處理海量資料(上)
ArrayList -> CopyOnWriteArrayList

  • 拷貝陣列過大,容易造成young GC FUll GC
  • 不適用於實時讀的場景,適合讀取多寫少的場景。
  • 實現讀寫分離,滿足最終一致性,使用的時候另外開闢空間。
  • 讀取未加鎖,寫加鎖。
    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }
    public E get(int index) {
        return get(getArray(), index);
    }
複製程式碼

HashSet、TreeSet --> CopyOnWriteArraySet、ConcurrentSkipListSet
ConcurrentSkipListSet對批量操作不能保證原子性。
參考:JDK1.8原始碼分析之ConcurrentSkipListSet(八)

HashMap、TreeMap --> ConcurrentHashMap、ConcurrentSkipListMap
ConcurrentHashMap效率相對比ConcurrentSkipListMap高,ConcurrentSkipListMap有些其不具有的特性:

  • ConcurrentSkipListMap 的key有序
  • 支援更高的併發

ConcurrentHashMap
相比於HashTable採取更為高效的分段鎖。ConcurrentHashMap裡包含了一個Segment陣列,一個Segment裡包含了一個HashEntry陣列。
Segment是一種可重入鎖,扮演鎖的角色;HashEntry則用於儲存鍵值對資料。加鎖/解鎖是在Segment上。

ConcurrentLinkedQueue
非阻塞演算法實現執行緒安全的佇列。由head節點和tail節點組成,每個節點Node由節點元素item和指向下一個節點的next的引用組成,節點與節點之間同個這個next關聯起來,組成連結串列結構的佇列。
參考:
Java多執行緒(四)之ConcurrentSkipListMap深入分析
探索 ConcurrentHashMap 高併發性的實現機制

6、J.U.C之AQS AbstractQueuedSynchronizer-AQS

從併發程式設計到分散式系統-如何處理海量資料(上)

  • 使用Node實現FIFO佇列,可以用於構建鎖或者其它同步裝置的基礎框架。
  • 利用了一個int型別表示狀態。
  • 使用方法是繼承。
  • 子類通過繼承並通過實現它的方法管理鎖的狀態,對應AQS中acquire和release的方法操縱鎖狀態。
  • 可以同時實現排它鎖和共享鎖模式(獨佔、共享)

AQS同步元件 1)等待多執行緒完成的CountDownLatch(JDK1.5)

從併發程式設計到分散式系統-如何處理海量資料(上)
允許一個或多個執行緒等待其他執行緒完成操作。
其建構函式接收一個int型別的引數作為計數器,呼叫countDown方法的時候,計數器的值會減1,CountDownLatch的await方法會阻塞當前執行緒,直到N變為零。
應用:平行計算,解析Excel中多個sheet的資料。
2)控制併發執行緒數的 Semaphore
用來控制同時訪問特定資源執行緒的數量。
應用:流量控制,特別是公共資源有限的場景,如資料庫連線。

//可用的許可的數量
Semaphore(int permits)
//獲取一個許可
aquire()
//使用完成後歸還許可
release()
//嘗試獲取許可證
tryAcquire()
複製程式碼

3)同步屏障 CyclicBarrier

從併發程式設計到分散式系統-如何處理海量資料(上)
讓一組執行緒達到一個屏障(同步點)時被阻塞,直到最後一個執行緒到達屏障時,才會開門,所有被屏障攔截的執行緒才會繼續執行。
應用:多執行緒計算資料,最後合併計算結果的場景。

CyclicBarrier和CountDownLatch的區別

  • CountDownLatch計數器只能使用一次,CyclicBarrier可以呼叫reset()方法重置。所以CyclicBarrier可以支援更加複雜的場景,如發生錯誤後重置計數器,並讓執行緒重新執行。
//屏障攔截的執行緒數量
CyclicBarrier(int permits)
//已經到達屏障
await()
//CyclicBarrier阻塞執行緒的數量
getNumberWaiting()

複製程式碼

4)重入鎖 ReentrantLock (排他鎖:同時允許單個執行緒訪問。)
支援重進入的鎖,表示該鎖能夠支援一個執行緒對資源的重複加鎖。即實現重進入:任意執行緒獲取到鎖之後能夠再次獲取該鎖而不會被鎖阻塞。

  • 該鎖支援獲取鎖時的公平和非公平性選擇
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
複製程式碼

公平鎖就是等待時間最長的執行緒最優先獲取鎖,也就是說獲取鎖的是順序的(FIFO)。而非公平則允許插隊。
非公平因為不保障順序,則效率相對較高,而公平鎖則可以減少飢餓發生的概率。

  • 提供了一個Condition類,可以分組喚醒需要喚醒的執行緒。
  • 提供能夠中斷等待鎖的執行緒機制,lock.lockInterruptibly()

ReentrantReadWriteLock (讀寫鎖,實現悲觀讀取,同時允許多個執行緒訪問)
在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被堵塞。其維護了一對鎖,通過分離讀鎖、寫鎖,使得併發性比排他鎖有很大提升。
適用於讀多寫少的環境,能夠提供比排他鎖更好的併發與吞吐量。
不足:ReentrantReadWriteLock是讀寫鎖,在多執行緒環境下,大多數情況是讀的情況遠遠大於寫的操作,因此可能導致寫的飢餓問題。

StampedLock
是ReentrantReadWriteLock 的增強版,是為了解決ReentrantReadWriteLock的一些不足。
StampedLock讀鎖並不會阻塞寫鎖,設計思路也比較簡單,就是在讀的時候發現有寫操作,再去讀多一次。。 StampedLock有兩種鎖,一種是悲觀鎖,另外一種是樂觀鎖。如果執行緒拿到樂觀鎖就讀和寫不互斥,如果拿到悲觀鎖就讀和寫互斥。
參考: Java8對讀寫鎖的改進:StampedLock

5)Condition
Condition提供了類似Object的監視器方法,依賴Lock實現等待/通知模式。
await():當前執行緒進入等待狀態直到被通知或中斷,當前執行緒進入執行狀態且從await()方法返回。
signal():喚醒一個在Condition上的執行緒,該執行緒從等待方法返回前必須獲得與Condition相關聯的鎖。

參考:Java執行緒(九):Condition-執行緒通訊更高效的方式

6)FutureTask
用於非同步獲取執行結果或取消執行任務的場景。(實現基於AQS)
參考:
Java併發程式設計:Callable、Future和FutureTask
FutureTask的用法及兩種常用的使用場景

7)Fork/Join
並行執行任務,即把大任務分割成若干小任務並行執行,最後彙總成大任務結果的框架。

  • 工作竊取演算法
    指的是某個執行緒從其他佇列裡竊取任務來執行。即這個佇列先幹完活,再去幫別人乾點。

參考:Fork/Join 模式高階特性

8)BlocklingQueue
阻塞佇列是一個支援兩個附加操作的佇列。 1)阻塞插入:當佇列滿的時候,佇列會阻塞插入元素的執行緒,直到佇列不滿。 2)阻塞移除:當佇列為空時,獲取元素的執行緒就會等待佇列變為非空。

通常用於生產者和消費者場景。生產者是向佇列裡新增元素的執行緒,消費者是從列裡獲取元素的執行緒。阻塞佇列就是生產者放元素,消費者獲取元素的容器。(FIFO)

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue
  • DelayQueue
  • SynchronousQueue
  • LinkedTransferQueue
  • LinkedBlockingDeque

參考:Java中的阻塞佇列

7、執行緒與執行緒池
在最後我們聊一下執行緒建立的相關問題?

執行緒:程式中一條獨立執行的線索,可以理解成程式中獨立執行的子任務。
程式:一旦程式執行起來就變成了作業系統當中的一個程式。

執行緒的建立:
1、繼承Thread類

public class TestThread extends Thread{
    @Override
    public void run(){
        
    }
}
複製程式碼
侷限性:Java單根繼承,不易於擴充套件。  
複製程式碼

2、實現Runnabl介面

public class TestThread implements Runnable{
    @Override
    public void run(){
        
    }
}
複製程式碼

3、實現Callable
執行Callable任務可以拿到一個Future物件,進行非同步計算。

public class TestThread implements Callable<E>{
    @Override
    public E call() throws Exception{
        
    }
}
複製程式碼

狀態:

從併發程式設計到分散式系統-如何處理海量資料(上)
為了保證共享資料的完整性,Java中引入互斥鎖的概念。即每個物件對應一個“互斥鎖”的標記(monitor),用來保證任何時刻,只能有一個執行緒訪問該物件。利用Java中每個物件都擁有唯一的一個監視鎖(monitor),當執行緒擁有這個標記才會允許訪問這個資源,而未拿到標記則進入阻塞,進入鎖池。 每個物件都有自己的一個鎖池的空間,用於存放等待執行緒。由系統決定哪個執行緒拿到鎖標記並執行。

方法:
currentThread():當前呼叫執行緒的相關資訊。
isAlive():判斷當前執行緒是否處於活動狀態。
getId():執行緒的唯一標識。
interrupted():測試當前執行緒是否已經中斷。
注:執行緒終端狀態由該方法清除,意味著連續兩次執行此方法,第二次將返回false。
isInterrupted():測試執行緒是否已經中斷。
注:不清楚狀態標誌。
run(): 執行緒執行的具體方法,執行完成的會進入消亡狀態。
start():使縣城出局就緒狀態,等待呼叫執行緒的物件執行run()方法。
sleep():讓當前執行緒放棄CPU時間片直接返回就緒狀態。 yield():讓當前執行緒放棄CPU時間片直接返回就緒狀態。但放棄的時間片不確定,可能剛剛放棄,便立即獲取。

執行緒通訊
join(): 讓當前執行緒邀請呼叫方法的執行緒優先執行,在被邀請的執行緒執行結束之前,邀請別人的執行緒不再執行,處於阻塞狀態,直到被邀請的執行緒執行結束之後,進入就緒狀態。
interrupt(): 中斷、打斷執行緒的阻塞狀態。直接讓阻塞狀態的執行緒返回就緒,由sleep()、join()導致的阻塞立刻解除。
wait():使當前執行程式碼的執行緒放棄monitor並進入等待狀態,直到接收到通知或被中斷為止(notify)。即此時執行緒將釋放自己的所有鎖標記和CPU佔用,同時進入這個物件的等待池(阻塞狀態)。只能在同步程式碼塊中呼叫(synchronized)。
notify():在等待池中隨機喚醒一個執行緒,放入鎖池,物件處於等待狀態,直到獲取物件的鎖標記為止。 只能在同步程式碼塊中呼叫(synchronized)。

4、執行緒池
1)newCachedThreadPool
建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。 潛在問題執行緒如果建立過多可能記憶體溢位。 2)newFixedThreadPool
建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。
3)newScheduledThreadPool
建立一個定長執行緒池,支援定時及週期性任務執行。
4)newSingleThreadExecutor
建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。

參考:Java 四種執行緒池的用法分析

暫且總結到這裡。此外本文意在給給大家提供學習的大體思路。其中有很多要點筆者並未去深入剖析,比如ConcurrentHashMap,這塊網路上有很多例子,可以參詳。筆者也給出了很多不錯的參考,大家可以根據個人需要點選閱讀。如需詳細瞭解,需具體閱讀原始碼,多實踐。

願與大家一同努力!加油!

參考資料:
《深入理解Java虛擬機器》
《Java併發程式設計藝術》
《Java多執行緒程式設計核心技術》

相關文章