目录
  1. 1. 原子操作的实现与原理
    1. 1.1. CPU术语
    2. 1.2. Java实现原子操作的原理
      1. 1.2.1. 使用循环CAS实现原子操作
      2. 1.2.2. CAS实现原子操作的三大问题
        1. 1.2.2.1. ABA问题
        2. 1.2.2.2. 循环时间长开销大
        3. 1.2.2.3. 只能保证一个共享变量的原子操作
      3. 1.2.3. 使用锁机制实现原子操作
原子操作的实现与原理

原子操作的实现与原理

  原子(atomic)本意是”不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作”.

CPU术语

名称 英文 解释
缓存行 Cache line 缓存的最小操作单位
比较并交换 Compare and Swap CAS操作需要输入两个值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换
CPU流水线 CPU pipeline CPU流水线的工作方式就像工业生产上的装配流水线,在CPU中由5至6个不同功能的电路单元组成一条指令处理流水线,然后将一条X86指令分成5~6步后再由电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度
内存顺序冲突 Memory order violation 内存顺序冲突一般是由假共享引起的,假共享是由多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存冲突时,CPU必须清空流水线

Java实现原子操作的原理

在Java中可以通过锁和循环CAS的方式来实现原子操作

使用循环CAS实现原子操作

  JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的.自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止,以下代码实现了一个基于CAS线程安全的计数器方法safeCount和一个非线程安全的计数器count

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Counter {
private AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;

public static void main(String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<Thread>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
cas.count();
cas.safeCount();
}
}
});
ts.add(t);
}

for (Thread t : ts) {
t.start();
}

// 等待所有线程执行完成
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.println(cas.i);
System.out.println(cas.atomicI.get());
System.out.println(System.currentTimeMillis() - start);
}

/** 使用CAS实现线程安全计数器 */
private void safeCount() {
for (;;) {
int i = atomicI.get();
boolean suc = atomicI.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}

/** 非线程安全计数器*/
private void count() {
i++;
}
}

  从Java 1.5开始,JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值).这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和自减1

CAS实现原子操作的三大问题

  在Java并发包中有一些并发框架也使用了自旋CAS的方式来实现原子操作,比如LinkedTransferQueue类的Xfer方法.CAS虽然很高效地解决了原子操作,但是CAS仍然存在三大问题.ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作

ABA问题

  因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了.ABA问题的解决思路就是使用版本号.在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A.从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题.这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值

1
2
3
4
5
6
public boolean compareAndSet(
V expectedReference, // 预期引用
V newReference, // 更新后的引用
int expectedStamp, // 预期标志
int newStamp // 更新后的标志
)

循环时间长开销大

  自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销.如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升.pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率

只能保证一个共享变量的原子操作

  当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁.还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作.比如:有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij.从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作

使用锁机制实现原子操作

  锁机制保证了只有获得锁的线程才能够操作锁定的内存区域.JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁.有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁

文章作者: Eric Liang
文章链接: https://ericql.github.io/2019/11/12/02-Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/01-%E5%BA%94%E7%94%A8%E7%AF%87/06-%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C%E7%9A%84%E5%AE%9E%E7%8E%B0%E4%B8%8E%E5%8E%9F%E7%90%86/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Eric Liang
打赏
  • 微信
  • 支付宝