原子类:无锁工具类的典范

利用原子类解决累加器问题

public class Test {
  AtomicLong count = 
    new AtomicLong(0);
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count.getAndIncrement();
    }
  }
}

无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状 态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性。

无锁方案的实现原理

其实原子类性能高的秘密很简单,硬件支持而已。CPU 为了解决并发问题,提供了 CAS 指 令(CAS,全称是 Compare And Swap,即“比较并交换”)。CAS 指令包含 3 个参 数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地 址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令, CAS 指令本身是能够保证原子性的。

使用 CAS 来解决并发问题,一般都会伴随着自旋,而所谓自旋,其实就是循环尝试。例如,实现一个线程安全的count += 1操作,“CAS+ 自旋”的实现方案如下所示,首先计 算 newValue = count+1,如果 cas(count,newValue) 返回的值不等于 count,则意味着 线程在执行完代码1处之后,执行代码2处之前,count 的值被其他线程更新过。那此时 该怎么处理呢?可以采用自旋方案,就像下面代码中展示的,可以重新读 count 最新的值 来计算 newValue 并尝试再次更新,直到成功。

class SimulatedCAS{
  volatile int count;
  // 实现 count+=1
  addOne(){
    do {
      newValue = count+1; //①
    }while(count !=
      cas(count,newValue) //②
  }
  // 模拟实现 CAS,仅用来帮助理解
  synchronized int cas(
    int expect, int newValue){
    // 读目前 count 的值
    int curValue = count;
    // 比较目前 count 值是否 == 期望值
    if(curValue == expect){
      // 如果是,则更新 count 的值
      count= newValue;
    }
    // 返回写入前的值
    return curValue;
  }
}

tips

ABA 问 题

前面我们提到“如果 cas(count,newValue) 返回的值不等于count,意味着线程在执行完 代码1处之后,执行代码2处之前,count 的值被其他线程更新过”,那如果 cas(count,newValue) 返回的值等于count,是否就能够认为 count 的值没有被其他线程更新过呢?显然不是的,假设 count 原本是 A,线程 T1 在执行完代码1处之后,执行代 码2处之前,有可能 count 被线程 T2 更新成了 B,之后又被 T3 更新回了 A,这样线程 T1 虽然看到的一直是 A,但是其实已经被其他线程更新过了,这就是 ABA 问题。
可能大多数情况下我们并不关心 ABA 问题,例如数值的原子递增,但也不能所有情况下都 不关心,例如原子化的更新对象很可能就需要关心 ABA 问题,因为两个 A 虽然相等,但是 第二个 A 的属性可能已经发生变化了。所以在使用 CAS 方案的时候,一定要先 check 一 下。

原子类概览

原子类:无锁工具类的典范_第1张图片
原子类组成概览图
1. 原子化的基本数据类型
2. 原子化的对象引用类型

对象引用的更新需要重点关注 ABA 问题,AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。
解决 ABA 问题的思路其实很简单,增加一个版本号维度就可以了,这个和乐观锁机制很类似,每次执行 CAS 操作,附加再更新一个版本号,只要保证版本号是递增的,那么即便 A 变成 B 之后再变回 A,版本号也不会变回来)。

3. 原子化数组

可以原子化地更新数组里面的每一个元素。

4. 原子化对象属性更新器

可以原子化地更新对象的属性

5. 原子化的累加器

仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。

你可能感兴趣的:(原子类:无锁工具类的典范)