JDK1.5之后的java.util.concurrent.atomic包里,多了一批原子处理类。主要用于在高并发环境下的高效程序处理。
网上关于这个原理介绍的比较靠谱的一片文章是出自IBM工程师的一篇:
值得一看。
这里,我们来看看AtomicInteger是如何使用非阻塞算法来实现并发控制的。
AtomicInteger的关键域只有一下3个:
-
- private static final Unsafe unsafe = Unsafe.getUnsafe();
- private static final long valueOffset;
- private volatile int value;
这里, unsafe是java提供的获得对对象内存地址访问的类,注释已经清楚的写出了,它的作用就是在更新操作时提供“比较并替换”的作用。实际上就是AtomicInteger中的一个工具。
valueOffset是用来记录value本身在内存的便宜地址的,这个记录,也主要是为了在更新操作在内存中找到value的位置,方便比较。
注意:value是用来存储整数的时间变量,这里被声明为volatile,就是为了保证在更新操作时,当前线程可以拿到value最新的值(并发环境下,value可能已经被其他线程更新了)。
这里,我们以自增的代码为例,可以看到这个并发控制的核心算法:
-
-
-
-
-
- public final int incrementAndGet() {
- for (;;) {
-
- int current = get();
- int next = current + 1;
- if (compareAndSet(current, next))
- return next;
- }
- }
-
- public final boolean compareAndSet(int expect, int update) {
-
- return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
- }
好了,看到这个代码,基本上就看到这个类的核心了。相对来说,其实这个类还是比较简单的。
----------------------------------------------------------------------------------------------
之前看了java8的longadder实现,最近又看到一篇文章介绍longadder实现的。其实现思路也是分段,最后需要get的时候,再进行sum计算。其核心思路就是减少并发,但之前老的Atomic,难道就没有提升的空间了吗?昨晚进行了一次测试。测试代码如下:
02 | * Atomically increments by one the current value. |
04 | *@return the updated value |
06 | public final int incrementAndGet() { |
12 | int next = current + 1 ; |
14 | if (compareAndSet(current, next)) |
以incrementAndGet为例,在非常高的并发下,compareAndSet会很大概率失败,因此导致了此处cpu不断的自旋,对cpu资源的浪费
既然知道此地是高并发的瓶颈,有什么办法呢?
01 | public class AtomicBetter { |
03 | AtomicInteger ai= new AtomicInteger(); |
05 | public int incrementAndGet() { |
09 | int current =ai.get(); |
11 | int next = current + 1 ; |
13 | if (compareAndSet(current, next)) |
33 | private boolean compareAndSet(intcurrent,intnext) { |
35 | if (ai.compareAndSet(current, next)) { |
41 | LockSupport.parkNanos( 1 ); |
很简单,当cas失败后,对线程park,减少多线程竞争导致的频繁cas失败,更进一步的导致cpu自旋,浪费cpu的运算能力。在4核虚拟机,Intel(R) Xeon(R) CPU E5-2630 0 @ 2.30GHz linux 2.6.32,(注意,老版本的内核,不支持高的精度ns级) 进行测试,同样都起4个线程,每个线程里面对AtomicInteger进行5kw次的incrementAndGet。原生的AtomicInteger,耗时14232ms,进行了35870次上下文切换,总共87967770955次时钟周期。那prak 1ns下呢,耗时5195ms,进行了19779次上下文切换,总共36187480878次时钟周期,明显性能上比原生的AtomicInteger更好,那这个park多少合适呢?那就只有人肉测试了
park | time(ms) | context-switches | cycles |
AtomicInteger | 14232 | 35,870 | 87,967,770,955 |
1ns | 5195 | 19,779 | 36,187,480,878 |
10ns | 5050 | 20,223 | 34,839,351,263 |
100ns | 5238 | 20,724 | 37,250,431,417 |
125ns | 4536 | 47,479 | 26,149,046,788 |
140ns | 4008 | 100,022 | 18,342,728,517 |
150ns | 3864 | 110,720 | 16,146,816,453 |
200ns | 3561 | 125,694 | 11,793,941,243 |
300ns | 3456 | 127,072 | 10,200,338,988 |
500ns | 3410 | 132,163 | 9,545,542,340 |
1us | 3376 | 134,463 | 9,125,973,290 |
5us | 3383 | 122,795 | 9,009,226,315 |
10us | 3367 | 113,930 | 8,905,263,507 |
100us | 3391 | 50,925 | 8,359,532,733 |
500us | 3456 | 17,225 | 8,096,303,146 |
1ms | 3486 | 10,982 | 7,993,812,198 |
10ms | 3456 | 2,600 | 7,845,610,195 |
100ms | 3555 | 1,020 | 7,804,575,756 |
500ms | 3854 | 822 | 7,814,209,077 |



本机环境下,park 1ms下,相对耗时,cs次数来说是最好的。因此这种优化要达到最佳效果,还要看cpu的情况而定,不是一概而定的
两个问题:
1、cas失败对线程park的副作用是什么。
2、如果park的时间继续加大,那会是这么样的结果呢。
(全文完)如果您喜欢此文请点赞,分享,评论。
转发:http://ifeve.com/better_atomicinteger/