JDK1.8 LongAdder 空间换时间: 比AtomicLong还高效的无锁实现
我们知道,AtomicLong的实现方式是内部有个value 变量,当多线程并发自增,自减时,均通过CAS 指令从机器指令级别操作保证并发的原子性。
// setup to use Unsafe.compareAndSwapLong for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset; /**
* Records whether the underlying JVM supports lockless
* compareAndSwap for longs. While the Unsafe.compareAndSwapLong
* method works in either case, some constructions should be
* handled at Java level to avoid locking user-visible locks.
*/
static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8(); /**
* Returns whether underlying JVM supports lockless CompareAndSet
* for longs. Called only once and cached in VM_SUPPORTS_LONG_CAS.
*/
private static native boolean VMSupportsCS8(); static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
} private volatile long value; /**
* Creates a new AtomicLong with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicLong(long initialValue) {
value = initialValue;
} /**
* Creates a new AtomicLong with initial value {@code 0}.
*/
public AtomicLong() {
}
先看LongAdder的add()方法:
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);
}
}
Cell是Striped64的一个内部类,顾名思义,Cell 代表了一个最小单元,这个单元有什么用,稍候会说道。先看定义:
/**
* Padded variant of AtomicLong supporting only raw accesses plus CAS.
*
* JVM intrinsics note: It would be possible to use a release-only
* form of CAS here, if it were provided.
*/
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
} // Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
Cell内部有一个非常重要的value变量,并且提供了一个更新其值的cas()方法。
回到add方法:
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);
}
}
这里,我有个疑问,AtomicLong已经使用CAS指令,非常高效了(比起各种锁),LongAdder如果还是用CAS指令更新值,怎么可能比AtomicLong高效了? 何况内部还这么多判断!!!
这是我开始时最大的疑问,所以,我猜想,难道有比CAS指令更高效的方式出现了? 带着这个疑问,继续。
第一if 判断,第一次调用的时候cells数组肯定为null,因此,进入casBase方法:
/**
* CASes the base field.
*/
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
原子更新base没啥好说的,如果更新成功,本地调用开始返回,否则进入分支内部。
什么时候会更新失败? 没错,并发的时候,好戏开始了,AtomicLong的处理方式是死循环尝试更新,直到成功才返回,而LongAdder则是进入这个分支。
分支内部,通过一个Threadlocal变量threadHashCode 获取一个HashCode对象,该HashCode对象依然是Striped64类的内部类,看定义:
/**
* Returns the probe value for the current thread.
* Duplicated from ThreadLocalRandom because of packaging restrictions.
*/
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
有个code变量,保存了一个非0的随机数随机值。
回到add方法:
拿到该线程相关的HashCode对象后,获取它的code变量,as[(n-1)&h] 这句话相当于对h取模,只不过比起取模,因为是 与 的运算所以效率更高。
计算出一个在Cells 数组中当先线程的HashCode对应的 索引位置,并将该位置的Cell 对象拿出来用CAS更新它的value值。
看到这里我想应该有很多人明白为什么LongAdder会比AtomicLong更高效了,没错,唯一会制约AtomicLong高效的原因是高并发,
高并发意味着CAS的失败几率更高, 重试次数更多,越多线程重试,CAS失败几率又越高,变成恶性循环,AtomicLong效率降低。
那怎么解决?
LongAdder给了我们一个非常容易想到的解决方案:减少并发,将单一value的更新压力分担到多个value中去,降低单个value的 “热度”,分段更新!!!
这样,线程数再多也会分担到多个value上去更新,只需要增加value就可以降低 value的 “热度” AtomicLong中的 恶性循环不就解决了吗?
cells 就是这个 “段” cell中的value 就是存放更新值的, 这样,当我需要总数时,把cells 中的value都累加一下不就可以了么!!
当然,聪明之处远远不仅仅这里,在看看add方法中的代码,casBase方法可不可以不要,直接分段更新,上来就计算 索引位置,然后更新value?
答案是不好,不是不行,因为,casBase操作等价于AtomicLong中的CAS操作,要知道,LongAdder这样的处理方式是有坏处的,分段操作必然带来空间上的浪费,
可以空间换时间,但是,能不换就不换,看空间时间都节约~! 所以,casBase操作保证了在低并发时,不会立即进入分支做分段更新操作,因为低并发时,
casBase操作基本都会成功,只有并发高到一定程度了,才会进入分支,
所以,Doug Lea对该类的说明是: 低并发时LongAdder和AtomicLong性能差不多,高并发时LongAdder更高效!
总结
1. base有没有参与汇总?
base在调用intValue等方法的时候是会汇总
2. 如果cell被创建后,原来的casBase就不走了,会不会性能更差? base的顺序可不可以调换?
刚开始我想可不可以调换add方法中的判断顺序,比如,先做casBase的判断?
仔细思考后认为还是不调换可能更好,调换后每次都要CAS一下,在高并发时,失败几率非常高,并且是恶性循环,比起一次判断,
后者的开销明显小很多,还没有副作用(上一个问题,base变量在sum时base是会被统计的,并不会丢掉base的值)。因此,不调换可能会更好。
3. AtomicLong可不可以废掉?
虽然LongAdder在空间上占用略大,但是,它的性能已经足以说明一切了,无论是从节约空的角度还是执行效率上,AtomicLong基本没有优势了
4. guava 、netty里面都照搬了LongAdder的实现
出处:
cool shell : 从LONGADDER看更高效的无锁实现
JDK1.8 LongAdder 空间换时间: 比AtomicLong还高效的无锁实现的更多相关文章
- Redis学习笔记~关于空间换时间的查询案例
回到目录 空间与时间 空间换时间是在数据库中经常出现的术语,简单说就是把查询需要的条件进行索引的存储,然后查询时为O(1)的时间复杂度来快速获取数据,从而达到了使用空间存储来换快速的时间响应!对于re ...
- Redis基础知识之————空间换时间的查询案例
空间与时间 空间换时间是在数据库中经常出现的术语,简单说就是把查询需要的条件进行索引的存储,然后查询时为O(1)的时间复杂度来快速获取数据,从而达到了使用空间存储来换快速的时间响应!对于redis这个 ...
- 你好,C++(28)用空间换时间 5.2 内联函数 5.3 重载函数
5.2 内联函数 通过5.1节的学习我们知道,系统为了实现函数调用会做很多额外的幕后工作:保存现场.对参数进行赋值.恢复现场等等.如果函数在程序内被多次调用,且其本身比较短小,可以很快执行完毕,那么 ...
- 计数排序(O(n+k)的排序算法,空间换时间)
计数排序就是利用空间换时间,时间复杂度O(n+k) n是元素个数,k是最大数的个数: 统计每个数比他小的有多少,比如比a[i]小的有x个,那么a[i]应该排在x+1的位置 代码: /* * @Auth ...
- leetcode-383-Ransom Note(以空间换时间)
题目描述: Given an arbitrary ransom note string and another string containing letters from all the magaz ...
- Elasticsearch实战 | 必要的时候,还得空间换时间!
1.应用场景 实时数据流通过kafka后,根据业务需求,一部分直接借助kafka-connector入Elasticsearch不同的索引中. 另外一部分,则需要先做聚类.分类处理,将聚合出的分类结果 ...
- HDU4548美素数——筛选法与空间换时间
对于数论的学习比较的碎片化,所以开了一篇随笔来记录一下学习中遇到的一些坑,主要通过题目来讲解 本题围绕:素数筛选法与空间换时间 HDU4548美素数 题目描述 小明对数的研究比较热爱,一谈到数,脑子里 ...
- 【C语言学习笔记】空间换时间,查表法的经典例子!知识就是这么学到的~
我们怎么衡量一个函数/代码块/算法的优劣呢?这需要从多个角度看待.本篇笔记我们先不考虑代码可读性.规范性.可移植性那些角度. 在我们嵌入式中,我们需要根据实际资源的情况来设计我们的代码.比如当我们能用 ...
- Cassandra 数据模型设计,根据你的查询来制定设计——反范式设计本质:空间换时间
转自:http://www.infoq.com/cn/articles/best-practice-of-cassandra-data-model-design 不要把Cassandra model想 ...
随机推荐
- 【原创】Linux基础之redhat6升级glibc-2.12到2.14
redhat6自带glibc-2.12,升级到glibc-2.14过程 # strings /lib64/libc.so.6 |grep GLIBC_GLIBC_2.2.5GLIBC_2.2.6GLI ...
- 【原创】大叔算法分享(4)Cardinality Estimate 基数计数概率算法
读过<编程珠玑>(<Programming Pearls>)的人应该还对开篇的Case记忆犹新,大概的场景是: 作者的一位在电话公司工作的朋友想要统计一段时间内不同的电话号码的 ...
- 【原创】大数据基础之ElasticSearch(5)重要配置及调优
Index Settings 重要索引配置 Index level settings can be set per-index. Settings may be: 1 static 静态索引配置 Th ...
- @Autowired mapper 层次 bean 带红线
在利用@Autowired 注解创建bean 时候 有时间会带有下滑红色横线 给人一种报错的感觉 下面是去除红线的办法 将颜色红色error 等级降低为黄色warn 即可
- 使用AltSearch格式化Kindle读书笔记
AltSearch是LibreOffice Writer的一个用于自动化执行复杂文本替换操作的扩展,能够在不需要复杂编程的条件下进行一些文档格式的手动与批量转换和调整.该扩展除了支持普通文本与正则表达 ...
- .net core2.x 自动注入 Entity(实体对象到上下文)
概要:有点老套,因为早在 .net frmework的时候(core还没出来),我们在使用 ef(4....6)的时候就已经这么用,这里我在搭建框架,所以随手写下,让后来人直接拿去用用. 1.使用前提 ...
- 关于hbase api的个人总结(带jar包)
1.如果本机代码没问题,jar包没问题的话,检查集群也没用问题,但是代码运行显示 找不到对应的映射主机,需要在 C:\Windows\System32\drivers\etc 中修改host文件, ...
- BZOJ4451 [Cerc2015]Frightful Formula 多项式 FFT 递推 组合数学
原文链接http://www.cnblogs.com/zhouzhendong/p/8820963.html 题目传送门 - BZOJ4451 题意 给你一个$n\times n$矩阵的第一行和第一列 ...
- ISP PIPLINE (五) Denoise
what is the Denoise? Denoise就是图像去噪,平滑图像,并保留图像细节. why does Denoise? 图像在采集并转换为数字信号的过程会引入一些噪声,这些噪声会让图片看 ...
- SpringCloud教程 | 第三篇: 服务消费者(Feign)
上一篇文章,讲述了如何通过RestTemplate+Ribbon去消费服务,这篇文章主要讲述如何通过Feign去消费服务.一.Feign简介 Feign是一个声明式的伪Http客户端,它使得写Http ...