最近看到了一个常用于并发编程的计数器类:AtomicInteger,这个类通过乐观锁实现了i++和i–的原子性和线程安全。具体看一下主要实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}

public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

getAndIncrement()就相当于i++的方法,代码很简单,关键就在于compareAndSet这个方法,里面执行了CAS操作:

首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。

这个操作是原子性的,调用的是JNI的接口,直接对应CPU的指令。
传入的参数valueOffset是value变量的内存地址(具体说是value变量相对AtomicInteger对象的起始地址的偏移量)。

说到这里基本完成了这个类的介绍,但是对于线程锁和CAS还有一些需要注意的问题:

1. CAS的ABA问题,就是如果V的值先由A变成B,再由B变成A怎么办?

在上面的代码并不会有所影响。但是在其他不能接受有中间状态或者是引用对象的情况下,很有可能第一个A和最后一个A是不一样的,因为CAS判断的是指针的地址,如果指针指向的内容发生的变化,或者是这个地址被重用了,就会发生“没锁住”的问题。
解决办法可以采用AtomicStampedReference或AtomicMarkableReference,前者将被锁的对象和一个版本号绑定一起,后者和一个标记位(boolean)绑定一起操作,这样就可以根据具体情况避免ABA问题了。

2. 已经用CAS了为什么还用volatile?

CAS解决的是原子性问题,而volatile解决的是变量可见性的问题:每个线程都有自己的working memory,与main memory不严格同步(机器指令重排序),因此高并发情况下可能会出现不一致。volatile的作用就避免指令重排序,强制修改立即同步到主内存中。(也可以理解为直接操作主存)
意思就是说,CAS可能compare的是working memory的数据,如果compare成功但是恰好working memory和main memory不同步,也会发生“没锁住”的问题。

3. 这种方式和synchronized{ i++; }有什么区别?

synchronized本身是一种独占锁,属于悲观锁(仅指JDK1.6以前的版本,之后的版本也使用了CAS实现从而得到了优化,较为复杂此处不展开叙述了),而上面的AtomicInteger是典型的乐观锁。前者是同时只能有一个线程操作,后者是通过校验不断循环尝试。二者没有绝对的好坏,在竞争激烈并发非常高的情况下悲观锁较好,在竞争较少的情况下乐观锁较好。