您的位置:首页 > 其它

最佳实践:AtomicInteger实现边界值控制

2015-10-10 09:31 316 查看

最佳实践:AtomicInteger实现边界值控制

前言

这篇文章主要讲两部分,一部分简单的讲了一下AtomicInteger和LongAdder的实现对比,这部分不会讲太细,因为有更好的文章已经讲过了,而且像一些实现细节可能自己看代码会更好一些。另外一部分就讲讲AtomicInteger实现边界值控制,也就是本篇的标题,其实说这个的时候我估计很多人不知道具体讲啥,等我细细讲来的时候估计就会明白。

LongAdder

在JDK8之前需要做本机的计数器操作,一般都会做AtomicInteger(注:接下来统一用AtomicInteger来讲,就不会讲AtomicLong,本质上是一样)来进行,因为AtomicInteger是在硬件层面做CAS操作,所以性能也是非常之高的。当然在JDK8中给我们提供了另外一种可能那就是LongAdder。开始对这个类还是满心期望。而且AtomicInteger的性能已经够好,难道LongAdder有比较它更好吗?在追求极限性能的过程中没有终点。废话不多说鸟,进入正题。

LongAdder和AtomicInteger实现对比

在说LongAdder的实现之前不得不先说说AtomicInteger性能瓶颈在哪里,直接看代码:

public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}


上面的代码是比较常用的incr操作,compareAndSet的实现是unsafe中的native实现CAS原子操作。这种原子操作的锁粒度非常小,能最大力度的减少锁等待的开销。但是这里也有一个很大的问题。可以看代码的最外层是一个for循环,当并发请求非常高的时候,失败率会非常高,CPU就会一直自旋等待,导致的结果就是性能下降。所以LongAdder要做的就是减少碰撞带来的性能损耗,其实现的思想和ConCurrentHashMap极其的相似,进行分桶,每一个小桶相互独立,这样就可以减少CAS的失败率从而提高性能,具体可以参见这篇文章:从LongAdder看更高效的无锁实现。我想大多数人看到这种实现既有种豁然开朗又有种似曾相识的感觉。对的,目前来看所有解决性能瓶颈的方向基本上都是朝着水平扩展的方向去,单点不行就多点。

LongAdder是否真的可以替代AtomicInteger

我一直信奉一句话:“古人有云,鱼和熊掌不可兼得。得到一些东西的同时总需要付出一些代价”。所以LongAdder在提高性能的同时也牺牲了一些东西。

public void add(long x) {
Cell[] as; long b, v; HashCode hc; Cell a; int n;
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
int h = (hc = threadHashCode.get()).code;
if (as == null || (n = as.length) < 1 ||
(a = as[(n - 1) & h]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
retryUpdate(x, hc, uncontended);
}
}
public void increment() {
add(1L);
}


刚才在上面没有讲代码,在这里一定要把代码贴出来,具体看increment操作,这里可以对比AtomicInteger的incrementAndGet操作,其区别是AtomicInteger有返回值,而LongAdder是没有返回值。可能大部分场景下并不需要关心实时的返回值。但是有些场景下就需要关心。比如说某些场景下需要关心边界值。比如说本地的库存扣减,需要严格校验库存为0这个值,LongAdder做不到这一点(其代码注释里面也讲了获取值并非原子性),从这里可以看出LongAdder并非在所有的场景下都能取代AtomicInteger。

AtomicInteger实现边界值控制

AtomicInteger并非自己实现了边界值控制,其实在某些场景下我们可能需要这样一种实现,直接上代码:

public final Result<Integer> incrementAndGet(int upperBound) {
for (;;) {
int current = get();
if(current>=upperBound){
//如果当前值已经到了最大值,则直接返回
return new Result<Integer>(COUNT_BOUNDS,current);
}
int next = current + 1;
if (compareAndSet(current, next)){
return new Result<Integer>(next);
}
}
}
public final Result<Integer> decrementAndGet(int lowerBound) {
for (;;) {
int current = get();
if(current<=lowerBound){
//如果当前值已经小于最小值,则直接返回
return new Result<Integer>(COUNT_BOUNDS,current);
}
int next = current - 1;
if (compareAndSet(current, next))
return new Result<Integer>(new Integer(next));
}
}


这样通过对AtomicInteger的一些简单的扩展就可以实现边界值控制,而且实现完全是原子操作。

LongAdder是否可以实现边界值的控制

这个问题我简单的想了一下,如果让LongAdder去实现这个功能,那么它就不是LongAdder了。这里先看一段代码:

public long sum() {
long sum = base;
Cell[] as = cells;
if (as != null) {
int n = as.length;
for (int i = 0; i < n; ++i) {
Cell a = as[i];
if (a != null)
sum += a.value;
}
}
return sum;
}


这段代码是获取LongAdder的值,注意这里并没有任何的锁操作,其实现也是把所有的桶的值都加起来。所以如果要实现边界值控制,就意味着每一次incr或者decr操作之前都要sum()一下,而且是需要加锁的,其性能显然不会高,会比AtomicInteger还要慢很多。这种尴尬的问题其实在单库分库分表之后也会碰到,当要count(*)的时候会发现实现不了或者成本比较大。所以也就如同我前面所说过的得到一些东西的同时总需要付出一些代价。LongAdder在追求极限性能的同时也牺牲了其他方面的一些可扩展性。

总结

学习一下LongAdder解决高并发下的性能问题,其主要思想是通过分桶来解决锁等待的问题。从LongAdder的源码也可以看出显然没有AtomicInteger那么丰富,为了性能也舍弃了很多的功能。所以目前来看LongAdder不会完全替代AtomicInteger,主要还取决于一些具体使用场景。

对AtomicInteger进行扩展实现边界值的控制,实现一些特殊需求,比如上面说的边界值控制。显然通过LongAdder是无法实现这种扩容。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: