JDK1.5之后的java.util.concurrent.atomic包里,多了一批原子处理类。主要用于在高并发环境下的高效程序处理。

网上关于这个原理介绍的比较靠谱的一片文章是出自IBM工程师的一篇:


流行的原子

 

值得一看。

这里,我们来看看AtomicInteger是如何使用非阻塞算法来实现并发控制的。

AtomicInteger的关键域只有一下3个:

 

1. // setup to use Unsafe.compareAndSwapInt for updates
2. private static final
3. private static final long
4. private volatile int


 这里, unsafe是java提供的获得对对象内存地址访问的类,注释已经清楚的写出了,它的作用就是在更新操作时提供“比较并替换”的作用。实际上就是AtomicInteger中的一个工具。

valueOffset是用来记录value本身在内存的便宜地址的,这个记录,也主要是为了在更新操作在内存中找到value的位置,方便比较。

注意:value是用来存储整数的时间变量,这里被声明为volatile,就是为了保证在更新操作时,当前线程可以拿到value最新的值(并发环境下,value可能已经被其他线程更新了)。

这里,我们以自增的代码为例,可以看到这个并发控制的核心算法:

 

1. /**
2.     * Atomically increments by one the current value.
3.     *
4.     * @return the updated value
5.     */
6. public final int
7. for
8. //这里可以拿到value的最新值
9. int
10. int next = current + 1;  
11. if
12. return
13.        }  
14.    }  
15.   
16. public final boolean compareAndSet(int expect, int
17. //使用unsafe的native方法,实现高效的硬件级别CAS
18. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
19.    }

 好了,看到这个代码,基本上就看到这个类的核心了。相对来说,其实这个类还是比较简单的。

----------------------------------------------------------------------------------------------


之前看了java8的longadder实现,最近又看到一篇文章介绍longadder实现的。其实现思路也是分段,最后需要get的时候,再进行sum计算。其核心思路就是减少并发,但之前老的Atomic,难道就没有提升的空间了吗?昨晚进行了一次测试。测试代码如下:


01
/**
 
  
02
* Atomically increments by one the current value.
 
  
03
*
 
  
04
*@return the updated value
 
  
05
*/
 
  
06
public final int incrementAndGet() {
 
  
07
 
 
  
08
for(;;) {
 
  
09
 
 
  
10
int current = get();
 
  
11
 
 
  
12
int next = current + 1;
 
  
13
 
 
  
14
if(compareAndSet(current, next))
 
  
15
 
 
  
16
return next;
 
  
17
 
 
  
18
}
 
  
19
}

以incrementAndGet为例,在非常高的并发下,compareAndSet会很大概率失败,因此导致了此处cpu不断的自旋,对cpu资源的浪费

既然知道此地是高并发的瓶颈,有什么办法呢?

01
public class AtomicBetter {
 
  
02
 
 
  
03
AtomicInteger ai=new AtomicInteger();
 
  
04
 
 
  
05
public int incrementAndGet() {
 
  
06
 
 
  
07
for(;;) {
 
  
08
 
 
  
09
int current =ai.get();
 
  
10
 
 
  
11
int next = current + 1;
 
  
12
 
 
  
13
if(compareAndSet(current, next))
 
  
14
 
 
  
15
return next;
 
  
16
 
 
  
17
}
 
  
18
 
 
  
19
}
 
  
20
 
 
  
21
/**
 
  
22
 
 
  
23
*如果cas失败,线程park
 
  
24
 
 
  
25
*@paramcurrent
 
  
26
 
 
  
27
*@paramnext
 
  
28
 
 
  
29
*@return
 
  
30
 
 
  
31
*/
 
  
32
 
 
  
33
private boolean compareAndSet(intcurrent,intnext) {
 
  
34
 
 
  
35
if(ai.compareAndSet(current, next)) {
 
  
36
 
 
  
37
return true;
 
  
38
 
 
  
39
}else{
 
  
40
 
 
  
41
LockSupport.parkNanos(1);
 
  
42
 
 
  
43
return false;
 
  
44
 
 
  
45
}
 
  
46
 
 
  
47
}
 
  
48
 
 
  
49
}

很简单,当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

 

AtomicInteger介绍_并发控制

 

AtomicInteger介绍_java_02

 

AtomicInteger介绍_并发控制_03

本机环境下,park 1ms下,相对耗时,cs次数来说是最好的。因此这种优化要达到最佳效果,还要看cpu的情况而定,不是一概而定的

两个问题:

1、cas失败对线程park的副作用是什么。

2、如果park的时间继续加大,那会是这么样的结果呢。

(全文完)如果您喜欢此文请点赞,分享,评论。