Treiber stack设计
最近看JDK11的CompletableFuture源码实现时,发现内部使用了Treiber stack,维基百科上作以下描述:
The Treiber stack algorithm is a scalable lock-free stack utilizing the fine-grained concurrency primitive compare-and-swap
Treiber stack算法是属于无锁并发栈,内部使用CAS(compare-and-swap)来实现无锁并发算法。关于CAS想必大家都很熟悉,至少会用。我们先来看看CompletableFuture的无锁并发栈的实现。
CompletableFuture的Treiber stack
在CompletableFuture内部,有一个成员变量stack,并且用volatile
修饰,这个属性是最作为栈的最顶端元素,入栈和出栈都要操作这个值,必然是会出现线程并发问题,volatile
是为了保证该值在多线程情况下内存可见性,且禁止指令重排序,但不保证原子性,所以对这个值的原子操作需要另外用到CAS,如下所示:
volatile Completion stack; // Top of Treiber stack of dependent actions
其中Completion
是一个抽象类,继承和实现了一大堆东西,这个我们不用去看,代码如下:
abstract static class Completion extends ForkJoinTask<Void> implements Runnable, AsynchronousCompletionTask { volatile Completion next; // Treiber stack link /** * Performs completion action if triggered, returning a * dependent that may need propagation, if one exists. * * @param mode SYNC, ASYNC, or NESTED */ abstract CompletableFuture<?> tryFire(int mode); /** Returns true if possibly still triggerable. Used by cleanStack. */ abstract boolean isLive(); public final void run() { tryFire(ASYNC); } public final boolean exec() { tryFire(ASYNC); return false; } public final Void getRawResult() { return null; } public final void setRawResult(Void v) {} }
注意其内部的next
也用volatile
修饰了,这是做了什么考虑呢? 具体的Completion
的子类我们也不用去看,我们来看下哪里用到这个类了,在postComplete
方法中,调用了JDK9引入的VarHandle的compareAndSet
方法,来做CAS,我们来看下postComplete
方法的源码:
/** * Pops and tries to trigger all reachable dependents. Call only * when known to be done. */ final void postComplete() { /* * On each step, variable f holds current dependents to pop * and run. It is extended along only one path at a time, * pushing others to avoid unbounded recursion. */ CompletableFuture<?> f = this; Completion h; while ((h = f.stack) != null || (f != this && (h = (f = this).stack) != null)) { CompletableFuture<?> d; Completion t; if (STACK.compareAndSet(f, h, t = h.next)) { if (t != null) { if (f != this) { pushStack(h); continue; } NEXT.compareAndSet(h, t, null); // try to detach } f = (d = h.tryFire(NESTED)) == null ? this : d; } } }
在postComplete
方法中,只针对入栈逻辑,入栈的代码如下:
/** Unconditionally pushes c onto stack, retrying if necessary. */ final void pushStack(Completion c) { do {} while (!tryPushStack(c)); } /** Returns true if successfully pushed c onto stack. */ final boolean tryPushStack(Completion c) { Completion h = stack; NEXT.set(c, h); // CAS piggyback return STACK.compareAndSet(this, h, c); }
所以简单来看,入栈的步骤如下:
- 尝试入栈,利用CAS将新的节点作为栈顶元素,新节点next赋值为旧栈顶元素
- 尝试入栈成功,即结束;入栈失败,继续重试上面的操作
抽象实现
上面JDK中CompletableFuture的Treiber stack实现感觉有点复杂,因为有其他逻辑掺杂,代码不容易阅读,其实抽象来看,Treiber stack首先是个单向链表,链表头部即栈顶元素,在入栈和出现过程中,需要对栈顶元素进行CAS控制,防止多线程情况下数据错乱。
对应入栈过程,伪代码如下:
void push(Node new) { do { } while(!tryPush(new)) // 尝试入栈 } boolean tryPush(node) { Node oldTop = top; node.next = oldTop; // 新节点next赋值为旧栈顶元素 return CAS(oldTop, node); // 利用CAS将新的节点作为栈顶元素 }
对于出栈,要做的工作就是将原来的栈顶节点移除,等待垃圾回收;新栈顶元素CAS为第一个子元素。伪代码:
E pop() { Node<E> oldTop = top; // 判断栈是否为空,为空直接返回 if (oldTop == null) return null; while (CAS(oldTop, oldTop.next)) { // CAS 将栈顶元素为第一个子元素 oldTop.next = null; // 旧的节点删掉next引用,等待gc } return oldTop.item; }
分析完上述入栈和出栈过程,完整代码如下:
import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.util.Objects; /** * 基于VarHandle实现TreiberStack * * @author mingshan */ public class TreiberStack<E> { private volatile Node<E> top; public void push(E item) { Objects.requireNonNull(item); Node<E> newTop = new Node<E>(item); do {} while (!tryPush(newTop)); } private boolean tryPush(Node<E> node) { Node<E> oldTop = top; NEXT.set(node, oldTop); return TOP.compareAndSet(this, oldTop, node); } public E pop() { Node<E> oldTop = top; if (oldTop == null) return null; while (TOP.compareAndSet(this, oldTop, oldTop.next)) { NEXT.set(oldTop, null); } return oldTop.item; } public boolean isEmpty() { return top == null; } public int size() { Node<E> current = top; int size = 0; while (current != null) { size++; current = current.next; } return size; } public E peek() { Node<E> eNode = top; if (eNode == null) { return null; } else { return eNode.item; } } @Override public String toString() { if (top == null) { return "Stack is empty"; } else { StringBuilder sb = new StringBuilder(); Node<E> current = top; while (current != null) { sb.append(current.item).append(","); current = current.next; } return sb.substring(0, sb.length() -1); } } private static class Node<E> { E item; Node<E> next; Node(E item) { this.item = item; } } private static final VarHandle TOP; private static final VarHandle NEXT; static { try { MethodHandles.Lookup l = MethodHandles.lookup(); TOP = l.findVarHandle(TreiberStack.class, "top", TreiberStack.Node.class); NEXT = l.findVarHandle(TreiberStack.Node.class, "next", TreiberStack.Node.class); } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); } // Reduce the risk of rare disastrous classloading in first call to // LockSupport.park: https://bugs.openjdk.java.net/browse/JDK-8074773 Class<?> ensureLoaded = LockSupport.class; } }
References:
title: Treiber stack设计
tags: [数据结构, 栈, JUC]
author: Mingshan
categories: [数据结构, 栈, JUC]
date: 2019-11-25
本文来自博客园,作者:mingshan,转载请注明原文链接:https://www.cnblogs.com/mingshan/p/17793625.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)