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)等。
