浅析 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.当条件满足后,操作后的状态如下:

原创文章 132 获赞 23 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_33797928/article/details/106165054