淺析 CompletableFuture的無鎖棧機制

這幾天在看公司的BinLogRelover的時候, 看到大佬在WIKI裏面特意指出,爲何使用Guava提供的ListenableFuture來做一些回調功能, 而且還指出在高併發的場景下Java8提供的CompletableFuture並不如人意,其實我個人還是比較喜歡CompletableFuture的, 哈哈, 可能是以前寫代碼用的比較多吧。

ListenableFuture用的不是很多, 所以優缺點也說不清楚, 但是CompletableFuture經常讓人詬病的大概包含兩點吧,

  1. cancel(true)
    大家使用CompletableFuture 肯定都是想使用他的異步編排能力,但是CompletableFuture在實現Future接口的同事,卻做了一些改變, 例如cancel()方法,一不小心就會踩坑。 在FutureTask中,cancel()會根據參數選擇是否中斷執行線程, 這一點在一些框架代碼中經常會用到,
   public boolean cancel(boolean mayInterruptIfRunning) {
        if (!(state == NEW &&
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {    // in case call to interrupt throws exception
            if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally { // final state
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
            finishCompletion();
        }
        return true;
    }

整體的執行流程也很清晰,也正是因爲此,一些人在使用FutureTask的時候,都會保留任務執行異常的時候,調用一下cancel(true), 進而中斷框架中執行線程。

但如果你切換到 CompletableFuture的時候, 可就要小心了,CompletableFuture的此方法可並不會做相同的功能。因爲在CompletableFuture中一切都是用AltResult來保存的,包括異常,中斷等信息。

 public boolean cancel(boolean mayInterruptIfRunning) {
        boolean cancelled = (result == null) &&
            internalComplete(new AltResult(new CancellationException()));
        postComplete();
        return cancelled || isCancelled();
    }

如果你之前在一些框架中特意處理中斷異常的話, 就要考慮下切換的時候如何保證手動觸發了。

  1. Completabk的無鎖棧機制究竟是否高效

這段代碼是Java併發實戰裏面提供的一段無鎖棧的實現代碼, 如果你非常瞭解Java的CAS機制的花,我像是很容易看懂的。

public class ConcurrentStack<E> {
    private AtomicReference<Node<E>> top = new AtomicReference<>();

    public void push(E item) {
        Node<E> newHead = new Node<>(item);
        Node<E> oldHead;
        do {
            oldHead = top.get();
            newHead.next = oldHead;
        } while (!top.compareAndSet(oldHead, newHead));
    }

    public E pop() {
        Node<E> oldHead;
        Node<E> newHead;
        do {
            oldHead = top.get();
            if (oldHead == null)
                return null;
            newHead = oldHead.next;
        } while (!top.compareAndSet(oldHead, newHead));
        return oldHead.item;
    }

    private static class Node<E> {
        public final E item;
        public Node<E> next;
        public Node(E item) {
            this.item = item;
        }
    }
}

原理

該算法的基本原理是:只有當您知道要添加的項目是自開始操作以來唯一添加的項目時,纔會添加新的項目。 這是通過使用比較和交換完成的。 在添加新項目時使用堆棧,將堆棧的頂部放在新項目之後。 然後,將這個新構造的頭元素(舊頭)的第二個項目與當前項目進行比較。 如果兩者匹配,那麼你可以將舊頭換成新頭,否則就意味着另一個線程已經向堆棧添加了另一個項目,在這種情況下,你必須再試一次。

當從堆棧中彈出一個項目時,在返回項目之前,您必須檢查另一個線程自操作開始以來沒有添加其他項目。

例如Push操作

  1. 首先單鏈表保存了各個Stack中的各個元素,成員變量top持有了棧的棧頂元素。
  2. 當執行push操作時,首先創建一個新的元素爲newHead,並讓該新節點的next指針指向top節點(此時top=oldHead)。
  3. 最後通過CAS替換top=newHead,CAS的交換條件是top=oldHead。
    當條件滿足後,操作後的狀態如下:

POP操作

  1. 當執行pop操作時,創建一個新的指針,該指針指向top的next元素。
  2. 然後通過CAS替換top=newHead,CAS的交換條件是top=oldHead
    3.當條件滿足後,操作後的狀態如下:

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