Java 并发 (14) -- 原子类

1. 简介

Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。所以,所谓原子类说简单点就是具有原子操作特征的类

JUC 包的原子类都存放在 JUC 的 atomic 子包下。根据操作的数据类型,可以将 JUC 包中的原子类分为 4 类:

  1. 基本类型

    1. AtomicBoolean:原子更新布尔类型
    2. AtomicInteger:原子更新整型
    3. AtomicLong:原子更新长整型
    4. Striped64:累加器
    5. LongAccumulator:Striped64 实现类
    6. LongAdder:Striped64 实现类
    7. DoubleAccumulator:Striped64 实现类
    8. DoubleAdder:Striped64 实现类
  2. 数组

    1. AtomicIntegerArray:原子更新整型数组里的元素
    2. AtomicLongArray:原子更新长整型数组里的元素
    3. AtomicReferenceArray:原子更新引用类型数组里的元素
  3. 引用类型

    1. AtomicReference:原子更新引用类型
    2. AtomicMarkableReference:原子更新带有标记位的引用类型
    3. AtomicStampedReference:原子更新带有版本号的引用类型
  4. 字段类(即对象的属性修改类型)

    1. AtomicIntegerFieldUpdater:原子更新整型字段的更新器
    2. AtomicLongFieldUpdater:原子更新长整型字段的更新器
    3. AtomicReferenceFieldUpdater:原子更新引用类型的字段

1. atomic 的原理

Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,不断重试,直到执行成功

Atomic 系列的类中的核心方法都会调用 unsafe 类中的几个本地方法。Unsafe 类全名为:sun.misc.Unsafe,这个类包含了大量 C 代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉我们这个类里面大量的方法的调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过 unsafe 分配内存的时候,如果自己指定某些区域可能会导致一些类似 C++ 一样的指针越界到其他进程的问题

2. 精讲

1. Striped64 类的实现

这个类是个抽象类,里面主要实现了两个方法 longAccumulate,doubleAccumulate 分别对 long 和 double 类型的数据进行一个累计。主要提供给 LongAdder 、LongAccumulator 、DoubleAdder 、DoubleAccumulator 进行一个调用。可以理解为用 cells 的方式来减少并发时产生的冲突。

源码:
Striped64 中的 Cell 类:

@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }
    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

Cell 类中仅有一个保存计数的变量 value,并且为该变量提供了 CAS 操作方法,Cell 类的实现虽然看起来很简单,但是它的作用是非常大的,它是 Striped64 实现分散计数的最为基础的数据结构,当然为了达到并发环境下的线程安全以及高效,Striped64 做了很多努力。Striped64 中有两个提供计数的 api 方法,分别为 longAccumulate和doubleAccumulate,两者的实现思路是一致的,只是前者对long类型计数,而后者对double类型计数。

longAccumulate 本身的逻辑并不复杂,只是因为使用无锁,使用了死循环,并包含了大量的检测代码,因为在理解过程中,一般不需要对具体的无锁技巧进行深度理解(如果水平到了也可以这样做),只需要结合其设计思想,理解设计思想即可。

final void longAccumulate(long x, LongBinaryOperator fn,
                         boolean wasUncontended) {
   int h;
   if ((h = getProbe()) == 0) { //获取当前线程的probe值,如果为0,则需要初始化该线程的probe值
       ThreadLocalRandom.current(); // force initialization
       h = getProbe();
       wasUncontended = true;
   }
   boolean collide = false;                // True if last slot nonempty
   for (;;) {
       Cell[] as; Cell a; int n; long v;
       if ((as = cells) != null && (n = as.length) > 0) { //获取cell数组
           if ((a = as[(n - 1) & h]) == null) { // 通过(hashCode & (length - 1))这种算法来实现取模
               if (cellsBusy == 0) {       // 如果当前位置为null说明需要初始化
                   Cell r = new Cell(x);   // Optimistically create
                   if (cellsBusy == 0 && casCellsBusy()) {
                       boolean created = false;
                       try {               // Recheck under lock
                           Cell[] rs; int m, j;
                           if ((rs = cells) != null &&
                               (m = rs.length) > 0 &&
                               rs[j = (m - 1) & h] == null) {
                               rs[j] = r;
                               created = true;
                           }
                       } finally {
                           cellsBusy = 0;
                       }
                       if (created)
                           break;
                       continue;           // Slot is now non-empty
                   }
               }
               collide = false;
           } 
           //运行到此说明cell的对应位置上已经有想相应的Cell了,不需要初始化了
           else if (!wasUncontended)       // CAS already known to fail
               wasUncontended = true;      // Continue after rehash
               
           //尝试去修改a上的计数,a为Cell数组中index位置上的cell
           else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
               break;
               
           //cell数组最大为cpu的数量,cells != as表面cells数组已经被更新了    
           else if (n >= NCPU || cells != as)
               collide = false;            // At max size or stale
           else if (!collide)
               collide = true;
           else if (cellsBusy == 0 && casCellsBusy()) {
               try {
                   if (cells == as) {      // Expand table unless stale
                       Cell[] rs = new Cell[n << 1]; //Cell数组扩容,每次扩容为原来的两倍
                       for (int i = 0; i < n; ++i)
                           rs[i] = as[i];
                       cells = rs;
                   }
               } finally {
                   cellsBusy = 0;
               }
               collide = false;
               continue;                   // Retry with expanded table
           }
           h = advanceProbe(h);
       }
       else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
           boolean init = false;
           try {                           // Initialize table
               if (cells == as) {
                   Cell[] rs = new Cell[2];
                   rs[h & 1] = new Cell(x);
                   cells = rs;
                   init = true;
               }
           } finally {
               cellsBusy = 0;
           }
           if (init)
               break;
       }
       else if (casBase(v = base, ((fn == null) ? v + x :
                                   fn.applyAsLong(v, x))))
           break;                          // Fall back on using base
   }
}
  1. longAccumulate 会根据当前线程来计算一个哈希值,然后根据算法 (hashCode & (length - 1)) 来达到取模的效果以定位到该线程被分散到的 Cell 数组中的位置
  2. 如果 Cell 数组还没有被创建,那么就去获取 cellBusy 这个共享变量(相当于锁,但是更为轻量级),如果获取成功,则初始化 Cell 数组,初始容量为 2,初始化完成之后将 x 保证成一个 Cell,哈希计算之后分散到相应的 index 上。如果获取 cellBusy 失败,那么会试图将 x 累计到 base 上,更新失败会重新尝试直到成功。
  3. 如果 Cell 数组以及被初始化过了,那么就根据线程的哈希值分散到一个 Cell 数组元素上,获取这个位置上的 Cell 并且赋值给变量 a,这个 a 很重要,如果 a 为 null,说明该位置还没有被初始化,那么就初始化,当然在初始化之前需要竞争 cellBusy 变量。
  4. 如果 Cell 数组的大小已经最大了(CPU的数量),那么就需要重新计算哈希,来重新分散当前线程到另外一个 Cell 位置上再走一遍该方法的逻辑,否则就需要对 Cell 数组进行扩容,然后将原来的计数内容迁移过去。这里面需要注意的是,因为 Cell 里面保存的是计数值,所以在扩容之后没有必要做其他的处理,直接根据 index 将旧的 Cell 数组内容直接复制到新的 Cell 数组中就可以了。

当然,上面的流程是高度概括的,longAccumulate 的实际分支还要更多,并且为了保证线程安全做的判断更多。longAccumulate会根据不同的状态来执行不同的分支,比如在线程竞争非常激烈的时候,会通过对cells数组扩容或者从新计算哈希值来重新分散线程,这些做法的目的是将多个线程的计数请求分散到不同的 cells 的 index 上,其实这和 java7 中的 ConcurrentHashMap 的设计思路是完全一致的,但是 java7 中的 ConcurrentHashMap 实现在 segment 加锁使用了比较重的 synchronized,而 Striped64 使用了 java 中较为底层的 Unsafe 类的 CAS 操作来进行并发操作,这种方式更为轻量级,因为它会不停的尝试,失败会返回,而加锁的方式会阻塞线程,线程需要被唤醒,这涉及到了线程的状态的改变,需要上下文切换,所以是比较重量级的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值