美文网首页
原子类:无锁工具类的典范

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

作者: woshishui1243 | 来源:发表于2019-12-01 18:35 被阅读0次

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

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. 原子化的基本数据类型
2. 原子化的对象引用类型

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

3. 原子化数组

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

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

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

5. 原子化的累加器

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

相关文章

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

    利用原子类解决累加器问题 无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁...

  • 21 | 原子类:无锁工具类的典范

    前面我们多次提到一个累加器的例子,示例代码如下。在这个例子中,add10K() 这个方法 不是线程安全的,问题就出...

  • python类进阶20160707

    继承 instance.class #查看实例所属的类在子类中可以重写父类的方法(名称相同),那么在子类里,父类原...

  • 一文读懂 Java 中的原子类

    一、无锁方案 Java 并发包中的原子类都是基于无锁方案实现的,相较于传统的互斥锁,无锁并没有加锁、解锁、线程切换...

  • Java随笔

    synchronized锁——为可重入锁,子类获取到对象的锁,父类也可以获取到该对象的锁。 该方法可以正常执行,而...

  • Java基础篇

    父类子类构造函数 子类的构造函数会隐式调用父类的无参构造函数,子类若想调用父类的构造函数需在子类的构造函数的第一行...

  • Java 并发

    目录 (1)基础概念 (2)线程 (3)锁 (4)同步器 (5)并发容器和框架 (6)Java并发工具类 (7)原...

  • Java8原子类的增强

    无锁的原子类操作使用系统的CAS指令,Java8中引入了LongAdder类,来进一步提高性能。 AtomicIn...

  • java子类调用父类构造器函数

    子类 调用 父类的构造函数:(构造函数不会被继承,只是被子类调用而已) 1、子类所有的 构造函数 默认调用父类的无...

  • java 利用反射动态调用自身方法及父类方法实例

    demo类: 反射工具类:ReflectionUtil.java父类:Animal.java子类1:Dog.jav...

网友评论

      本文标题:原子类:无锁工具类的典范

      本文链接:https://www.haomeiwen.com/subject/naxtgctx.html