Java 的 java.util.concurrent
包除了提供底层锁、并发集合外,还提供了一组原子操作的封装类,它们位于 java.util.concurrent.atomic
包。
以 AtomicInteger
为例,它提供的主要操作有:
- 增加值并返回新值:
int addAndGet(int delta)
- 加 1 后返回新值:
int incrementAndGet()
- 获取当前值:
int get()
- 用 CAS 方式设置:
int compareAndSet(int expect, int update)
Atomic 类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了 CAS:Compare and swap。
自己通过 CAS 编写 incrementAndGet()
,它大概长这样:
public int incrementAndGet(AtomicInteger var) {
int current, next;
do {
current = var.get(); // 获取当前值
next = prev + 1; // 当前值加 1
} while ( ! var.compareAndSwap(current, next)); // 如果 AtomicInteger 的当前值是 current,那就返回 next。否则继续循环等待
return next;
}
通过 CAS 和 do...while
循环配合,即使其他线程修改了 AtomicInteger
的值,最终的结果也是正确的。
虽然上面使用
do...while
看上去很麻烦,但是实际过程中比使用锁的速度更快。
可以利用 AtomicLong
可以编写一个多线程安全的全局唯一 ID 生成器:
class IdGenerator {
AtomicLong var = new AtomicLong(0);
public long getNextId() {
return var.incrementAndGet();
}
}
通常情况下,我们并不需要直接用 do ... while
循环调用 compareAndSet
实现复杂的并发操作,而是用 incrementAndGet()
这样的封装好的方法,因此,使用起来非常简单。
如果有大量线程要访问相同的原子值,性能会大幅下降,因为乐观更新需要太多次重试。Java SE 8 提供了 LongAdder
和 LongAccumulator
类来解决这个问题。LongAdder
包括多个变量(加数),其总和为当前值。可以有多个线程更新不同的加数,线程个数增加时会自动提供新的加数。通常情况下,只有当所有工作都完成之后才需要总和的值,对于这种情况,这种方法会很高效。性能会有显著的提升。
这里写一个使用小例子:
final LongAdder adder = new LongAdder();
for(...) {
pool.submit(() -> {
while(...) {
...
if (...) adder.incerment(); // 自增操作
}
});
}
...
long total = adder.sum(); // 获取总数
LongAccumulator
将这种思想推广到任意的累加操作。在构造器中,可以提供这个操作以及它的零元素:
LongAccumulator adder = new LongAccumulator(Long::sum, 0); // 操作符为 + ,零元素为 0
// In some thread...
adder.accumulate(value);
在内部,这个累加器包含变量 a1,a2,…,an。每个变量初始化为零元素(这个例子中零元素为 0 )。
调用 accumulate()
并提供值 v 时,其中一个变量会以原子方式更新为 ai=ai op v,这里 op 是中缀形式的累加操作。在我们这个例子中,调用 accumulate()
会对某个 i 计算 ai=ai+v。get()
的结果是 a1 op a2 op…op an。在我们的例子中,这就是累加器的总和:a1+a2+…+an。
DoubleAdder
和DoubleAccumulator
也采用同样的方式,只不过处理的是double
值。
小结
使用 java.util.concurrent.atomic
提供的原子操作可以简化多线程编程:
- 原子操作实现了无锁的线程安全;
- 适用于计数器(Adder),累加器(Accumulator)等。