分析LongAdder原理并实现它

1,585 阅读8分钟

分析LongAdder类主要的目的是看看其背后的实现是如何?为什么会比我们AtomicLong同志的效率要高?为什么需要LongAdder?其背后的思想是什么?

震惊!!! 开局两张图, 内容全靠编

从简单的入门起手分析源码

@Test
public void test() throws Exception {
    LongAdder longAdder = new LongAdder();
    longAdder.increment();
    System.out.println(longAdder.sum());
}

源码分析开始

现在我们单步调试分析下 LongAdder longAdder = new LongAdder(); 这行是构造方法,但是我发现其实他就初始化了 base(其实没初始化直接默认赋值 0 ) 比较重要还有一个 static final int NCPU = Runtime.getRuntime().availableProcessors();获取 cpu核心 的数量

底层源码是这样的

    public void add(long x) {
        Cell[] cs; long b, v; int m; Cell c;
        
        if ((cs = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            if (cs == null || (m = cs.length - 1) < 0 ||
                (c = cs[getProbe() & m]) == null ||
                !(uncontended = c.cas(v = c.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

!casBase(b = base, b + x) // 先试试使用 base cas 增加看下是否能够成功, 如果成功了后面就不需要搞那么复杂了

cs == null ----> 如果 cell 没有创建

(m = cs.length - 1) < 0 ----> cell != null 但是 cell 的长度不对

(c = cs[getProbe() & m]) == null ----> 计算出来的 cell 位置为空

!(uncontended = c.cas(v = c.value, v + x)) ----> 如果计算获取出来的 cell 数组中的元素不为空, 则尝试把 cell 数组中的那个元素 +x, 如果上面的条件都成立, 则会执行

longAccumulate(x, null, uncontended); 函数, 这个函数过于复杂(太难了)

分析下面这段代码很复杂, 而且还存在高并发方面的问题需要实时关注,否则光光看肯定是不行的

final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
    // 下面这段代码, 初始化了 h 和 wasUncontended = true
    int h;
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        wasUncontended = true;
    }
    // ==============================================================
    
    boolean collide = false;                // True if last slot nonempty, 如果为 true 表示最后一个槽(数组的元素位置)不是空的
    done: for (;;) {
        Cell[] cs; Cell c; int n; long v;
        // 如果 cells 不为空, 并且 cells 的长度大于 0 表示 cell 在这里已经做了初始化了, 并且已经创建了 cell 数组
        if ((cs = cells) != null && (n = cs.length) > 0) {
        	// 代码能进入这里说明了, cells 数组初始化完毕, 里面已经存在数据的元素了, 只不过这个元素是否有效就不知道了, 所以需要判断下
            if ((c = cs[(n - 1) & h]) == null) {
            	// 如果这个变量不等于 0 表示已经进入到后面的步骤了, 此时可能存在两个线程, 一个线程已经把cellsBusy设置成了 1 , 所以为了防止这种情况, 做下判断
                if (cellsBusy == 0) {       // Try to attach new Cell
                	// 新建已经 cell
                    Cell r = new Cell(x);   // Optimistically create
                    // 再次做下 cellsBusy 的判断,并且将其置为 1 , 表示现阶段 cas 功能正在执行中, 其他 cas 操作暂时不支持
                    if (cellsBusy == 0 && casCellsBusy()) {
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            // 下面这段代码就承上启下了, 类似于 单例的 两次检测 再次检测下, 是否满足条件
                            if ((rs = cells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                // 把新的 cell 赋值到 cells 中
                                rs[j] = r;
                                // 回到前面的 for 死循环
                                break done;
                            }
                        } finally {
                        	// 释放锁
                            cellsBusy = 0;
                        }
                        // 这段 if 语句主要是 cells 不能为null,并且 cells的长度已经被确认, 但是里面的值为空, 必须初始化的情况下
                        continue;           // Slot is now non-empty 槽位不是空的, 所以 continue 掉了
                    }
                }
                // 未发生碰撞, 最后一个槽不是空的
                collide = false;
            }
            // 说明还不存在竞争关系
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            // 如果读出来的 cells 其中一个 slot 不为空,则把那个 slot 的值读取出来进行 cas 增加
            else if (c.cas(v = c.value,
                           (fn == null) ? v + x : fn.applyAsLong(v, x)))
                break;
            // 下面的几个小  if , 能到这里的 cells 中一个 slot 不为空,并且前面对 slot 的 cas 失败了
            // 如果 cells 数组的长度大于等于 cpu 核心的数量 或者 cells 被其他函数修改了, 和 cs 不相等 cs 的值过时了
            else if (n >= NCPU || cells != cs)
                collide = false;            // At max size or stale
            else if (!collide) // 还能怎么办, cas 失败存在碰撞的情况
                collide = true;
            // 上 cell 忙锁
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                	// 防止 cs 的值过时
                    if (cells == cs)        // Expand table unless stale
                    	// 扩容 2 倍
                        cells = Arrays.copyOf(cs, n << 1);
                } finally {
                	// 释放cell 忙锁
                    cellsBusy = 0;
                }
                // 释放碰撞状态
                collide = false;
                continue;                   // Retry with expanded table
            }
            // 再获取新的 hash 值
            h = advanceProbe(h);
        }
        // 如果 cellsBusy 等于空;
        // cells 等于 cs (cell 在前面将自己的应用丢给cs后可能被其他函数修改, 所以需要这个判断, 保证cs还是最新的);
        // casCellsBusy 尝试修改 cellsBusy 为 1 如果成功说明不存在 冲突, 可以进行修改
        else if (cellsBusy == 0 && cells == cs && casCellsBusy()) {
            try {                           // Initialize table
            	/// 防止 cs 过期了
                if (cells == cs) {
                	// new 出 cells 数组
                    Cell[] rs = new Cell[2];
                    // 设置 cells 数组中的一个 slot
                    rs[h & 1] = new Cell(x);
                    // 把值给 cells
                    cells = rs;
                    // 回到 for 死循环
                    break done;
                }
            } finally {
            	// 释放 cells 忙锁
                cellsBusy = 0;
            }
        }
        // Fall back on using base
        // 如果无法命中前面的 if 则再次尝试修改 base 的值, 如果还是存在竞争则失败
        else if (casBase(v = base,
                         (fn == null) ? v + x : fn.applyAsLong(v, x)))
            break done;
    }
}

至此我们分析完毕了 LongAdder.add 源码的分析

现在看下 LongAdder.sum 源码

    public long sum() {
        Cell[] cs = cells;
        long sum = base;
        if (cs != null) {
            for (Cell c : cs)
                if (c != null)
                    sum += c.value;
        }
        return sum;
    }

这可真是太喜欢了. base + cell[0] + ... + cell[cell.length - 1]

总结下, 要不然有点乱

首先如果能够直接在 base 上累加那就在 base 上累加, 如果不能再 base 上累加, 那就看看 cells 数组是否被初始化, 以及里面的元素是否全部被初始化, 和扩容问题等等

  • 如果 cells 未被初始化, 则借助 cellsBusy 进行 cas 上锁, 然后 new 出 一个 rs 数组将数组赋值给 cells 完成第一次的初始化, 这次初始化会将 length 确定, 并且数组内部一个 slot 已经被填充

  • 如果 cells 被初始化, 则判断下 length 是否确定已经他的 slot 是否已经被填充, 如果 length 已经确定 则在确认下 slot 是否有空的, 如果存在空的, 直接 new 一个 新的 cell 给他填充, 填充过程为了防止多线程安全问题, 还是使用了我们的 cellsBusycas 锁保证线程安全( 当然是否真的 new 一个新的还是在另外的已经填充的 cell 上计算应该需要看 hash 的计算, 而计算方法在[1]这里当然这个还和线程的探测值有点关系, 没细讲)

[1]hash计算过程

static final int advanceProbe(int probe) {
    probe ^= probe << 13;   // xorshift
    probe ^= probe >>> 17;
    probe ^= probe << 5;
    THREAD_PROBE.set(Thread.currentThread(), probe);
    return probe;
}
  • 如果 cells 已经被 slot 槽已经填充完毕我们还可以扩容新的 slot 只要不满足这个条件就好 else if (n >= NCPU || cells != cs)还是借助了 cellsBusycas 锁, 然后 n << 1 的扩容 cells = Arrays.copyOf(cs, n << 1); (两倍扩容)

  • 在看那段源码你会发现他在最后一个 if 语句中还是做了一次对 base 的 cas else if (casBase(v = base, (fn == null) ? v + x : fn.applyAsLong(v, x)))

LongAdder 的优点

现在呢, 这样做的好处在哪?

其实很简单, cas 的有点很多, 无锁效率高, 内核把关, 但是他只能做的操作实在太少, 最致命的是如果遇到特别大的高并发你会发现它很多时候再做这样的判断

while (true) {
	int oldVal = memoryVal;
	if (U.cas(oldVal, newVal)) { // 如果 cas 成功, 隐藏的做了个 memoryVal = newVal
    	break;
    }
}

如果 cas 成功, 则直接跳出循环, 但是如果存在一大堆线程, 比如 10000 个, 那么只存在一个线程会完成 cas, 其他的线程会发现不满足条件, 再次获取主存中的 value 赋值给 oldVal 再做判断, 实际上会出现无数的死循环, 贼消耗 cpu核心 资源

所以 LongAdder 这种将 value 拆分成 base + cells[n].sum 的方式就此出现, 线程不再盯着一个 valuecas 了, 即使线程1cell[0] 做了 cascell[0] 从 1 变成 2 , 线程2cell[1] 从 0 cas 成了 1 , 在这个过程中我们建立了多个 茅坑(cell), 这样在抢占的时候就多了个机会, 即便最后你需要把屎拿出来称重, 只要把 茅坑1 和 茅坑2 的屎 加上 你自己 还没拉出来的一起称一称重量就行, 好处多多

LongAdder 缺点

但是坏处呢?

也是存在的, 就是 sum 的结果在某一个时刻可能是不准确的, 因为无法保证在计算 最终值的过程中茅坑中是否还会有人在拉屎, 还会不会有新的人在拉屎, 你不知道, 但直到最后, 你把茅坑炸了, 不让人拉屎了, 这个时候你称的重量才是最终总量(最终一致性)

注意开始着手写一个只记得 LongAdder 时, 我推荐先不急, 为了不让博客里面的图片很多, 我省略了对每个类的特写, 这步不能丢, 推荐看看 Cell 类, 你会发现 @jdk.internal.vm.annotation.Contended 注解, 其作用很明显, 我举个简单的梨子, 你房子是老旧房需要拆迁(缓存行A中的一个变量), 但你的房屋临近的还有另一栋新盖的房子(缓存行A的另一个变量, 实际一个变量不一定只占用一个byte, 还有注意下是同一个缓存行哦!), 你找到了一个足以拆掉附近64栋房子的炸药(缓存行大小64byte), 直接给他炸了, 你只能赔钱(无端端把人家的房子炸了你需要花费更多的时间叫他自己再建房子, 钱你付), 现在使用了Contended, 说明你有钱了, 把周围 64 byte的地全卖了, 除了中间你的房子, 其余都种了菜, 这样新的房子只能选择在这 64 byte之外的地方(另一个缓存行) 这样代价很大, 但在高并发环境下效率提高显著----------------- 对了, 我是 jdk 11 其他版本的 jdk 可能注解名字一样但包位置不同(可能)

自己动手写一个简单的 LongAdderBug ?(复习时刻)

Cell 类的创建过程分析

字段

我们知道他把我们的 value 分割成 basecells , 目前我们缺少了 Cells 类, 这个类的字段想起来应该很简单, 只要储存一个 value , 这个 value 为了防止多线程安全需要加上 volatile 并且这个类不需要对外公开? 对吧? 所以内部类走起

private volatile long value

方法

那么现在考虑他有哪些方法呢?

对一个字段的方法无非就几种, 读和写

  • 首先构造函数一个, 用于设置值给 value
public Cell(long value) {
    this.value = value;
}
  • 发现这个值需要做 addsub 但实际上这两个方法可以统一成 set, 但是他有可能是多线程操作的, 容易出现线程安全问题, 需要做 cas 操作进行, 同时需要返回值, 判断是否 cas 成功了

既然需要 cas 那么便可以使用 MethodHandlers 类进行操作, 为了防止别人以为我抄了代码, 这里我选择使用 Unsafe 类进行 cas 操作

听说 jdk11Unsafe 类可以直接 getUnsafe 方法获取, 但这里给给出了反射方式获取的代码吧, 虽然这段代码也很简单

现在我们考虑写的两个方法, 一个是写方法 compareAndSet 和另一个 getValue 方法

// @jdk.internal.vm.annotation.Contended // 这里需要特殊的方法导入, 现在我没做等下再做
private static class Cell {

    private volatile long value;

    public Cell(long value) {
        this.value = value;
    }

    public boolean compareAndSet(long expect, long newValue) {
        long fieldOffset = 0;
        try {
            fieldOffset = unsafe.objectFieldOffset(this.getClass().getDeclaredField("value"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        return unsafe.compareAndSwapLong(this, fieldOffset, expect, newValue);
    }

    public long getValue() {
        return this.value;
    }

    private static final Unsafe unsafe;

    static {
        // unsafe = Unsafe.getUnsafe();
        Class<Unsafe> unsafeClass = Unsafe.class;
        try {
            Field declaredField = unsafeClass.getDeclaredField("theUnsafe");
            declaredField.setAccessible(true);
            unsafe = (Unsafe) declaredField.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

}

LongAdderBug类的源码编写

字段

那我现在我们来想想新的类 LongAdderBug 方法需要哪些字段

base 逃不掉了, cells 也是, 还需要啥呢? 哦 还有一个 cas 的锁, 这里跟前面分析的一样使用 cellsBusy 做锁

这里我就不做 Unsafe 方法了, 太麻烦使用 MethodHandlers 获取吧

private volatile long base;
private volatile Cell[] cells;
private volatile boolean cellBusy;
private static final VarHandle BASE;
private static final VarHandle CELLBUSY;

public LongAdderBug() {
}

static {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    try {
        BASE = lookup.findVarHandle(LongAdderBug.class, "base", long.class);
        CELLBUSY = lookup.findVarHandle(LongAdderBug.class, "cellBusy", boolean.class);
    } catch (NoSuchFieldException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}

方法

在方法方面的考虑这个函数我就不考虑什么 reset 之类的东西直接上 自增 自减 还有加多少 统计下当前值是多少 就这几个方法吧, 简单点

而核心方法 add 思想其实很简单, 能使用 base 进行计算就直接使用它, 如果不能出现了 cas 冲突, 则去找 cells 多个数组进行 cas 计算, 等到合适的时候再去统计就好了, 如果 cell slot 为空, 则直接 new 一个新的 slot , 将 val 值传入构造函数的方法中

下面是全代码: 注意下 Contended 你可能要另外配置, 我 openJDK11 的包是 @jdk.internal.vm.annotation.Contended 这里 但是 jdk 1.8 好像不是, 你需要修改, 然后再添加一些虚拟机参数否则无法开启这个功能, jvm 默认关闭的

public class MyLongAdder {
	private volatile long base;
	private final Cell[] cells = new Cell[Runtime.getRuntime().availableProcessors() + 1];
	private volatile boolean cellBusy = false;
	private static final VarHandle BASE;
	private static final VarHandle CELLBUSY;
	
	public static void main(String[] args) throws Exception {
		ArrayList<MyLongAdder> bugs = new ArrayList<>();
		ExecutorService threadPool = Executors.newFixedThreadPool(100);
		int count = 10000;
		for (int j = 0; j < 1000; j++) {
			MyLongAdder adderBug = new MyLongAdder();
			CountDownLatch latch = new CountDownLatch(count);
			for (int i = 0; i < count; i++) {
				threadPool.submit(() -> {
					adderBug.increment();
					latch.countDown();
				});
			}
			latch.await();
			bugs.add(adderBug);
		}
		threadPool.shutdown();
		TimeUnit.SECONDS.sleep(10);
		bugs.stream().filter(myLongAdder -> {
			if (count == myLongAdder.sum()) {
				return false;
			}
			return true;
		}).forEach(myLongAdder -> System.out.println(myLongAdder.sum()));
		System.err.println("没有打印, 就代表着成功!!!");
	}
	
	
	public MyLongAdder() {
	}
	
	public long sum() {
		long sum = base;
		for (Cell cell : this.cells) {
			if (null != cell) {
				sum += cell.value;
			}
		}
		return sum;
	}
	
	public void add(long val) {
		
		long b = base;
		
		synchronized (this) {
			if (casBase(b, b + val)) {
				return;
			}
			for (; ; ) {
				Cell[] cs = this.cells;
				int index = Math.abs(ThreadLocalRandom.current().nextInt()) % cs.length;
				long cv;
				
				if (casBase(cv = base, cv + val)) {
					break;
				}
				else if (cs[index] == null) {
					if (!cellBusy && casCellsBusy()) {
						try {
							Cell r = new Cell(val);
							Cell[] rs = this.cells;
							if (rs[index] == null) {
								rs[index] = r;
							}
						} finally {
							cellBusy = false;
						}
					}
				}
				else if (cs[index].compareAndSet(cv = cs[index].value, cv + val)) {
					break;
				}
			}
		}
	}
	
	private boolean casCellsBusy() {
		return CELLBUSY.compareAndSet(this, false, true);
	}
	
	private boolean casBase(long expect, long val) {
		return BASE.compareAndSet(this, expect, val);
	}
	
	private boolean casBase(long val) {
		return (boolean) BASE.getAndAdd(this, val);
	}
	
	public void increment() {
		add(1L);
	}
	
	public void decremenet() {
		add(-1L);
	}
	
	static {
		MethodHandles.Lookup lookup = MethodHandles.lookup();
		try {
			BASE = lookup.findVarHandle(MyLongAdder.class, "base", long.class);
			CELLBUSY = lookup.findVarHandle(MyLongAdder.class, "cellBusy", boolean.class);
		} catch (NoSuchFieldException | IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}
	
	// TODO 这里需要做缓存行填充, 提高高并发效率, 但是需要特殊开启方法, 我使用的是 idea 按下快捷键就直接添加了  汗~~~
	@jdk.internal.vm.annotation.Contended
	private static class Cell {
		
		private volatile long value;
		
		public Cell(long value) {
			this.value = value;
		}
		
		public boolean compareAndSet(long expect, long newValue) {
			long fieldOffset = 0;
			try {
				fieldOffset = unsafe.objectFieldOffset(this.getClass().getDeclaredField("value"));
			} catch (NoSuchFieldException e) {
				e.printStackTrace();
			}
			return unsafe.compareAndSwapLong(this, fieldOffset, expect, newValue);
		}
		
		public long getValue() {
			return this.value;
		}
		
		private static final Unsafe unsafe;
		
		static {
			unsafe = Unsafe.getUnsafe();
		}
		
		@Override
		public String toString() {
			return "Cell{" + "value=" + value + '}';
		}
	}
}