原子类

原子类

阿里巴巴2021版JDK源码笔记(2月第三版).pdf

链接:https://pan.baidu.com/s/1XhVcfbGTpU83snOZVu8AXg
提取码:l3gy

concurrent包的结构层次:Atomic类 -> 锁与条件 -> 同步工具 -> 并发容器 -> 编程池 -> CpmpletableFuture

1. AtomicInteger和AtomicLong

1.1 源码及其原理

public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

查看源码可知,这里主要是通过CAS(compareAndSwap)实现

1.2 悲观锁和乐观锁

  • 悲观锁:悲观,开发人员认为数据发生并发冲突的概率很大,在读操作之前就上锁(正常都是读后写)

  • 乐关锁:乐观,开发人员认为数据发生并发冲突的概率比较小,读操作不上锁,写操作才上锁,判断数据在此期间是否被其 他线程修改了,CAS就是典型的乐观锁模式(判断数据是否被修改,同时写回新值,这两个操作要合成一个原子操作,也就是CAS)

1.3 Unsafe的CAS详解

unsafe.compareAndSwapInt(this, valueOffset, expect, update);

1.4 自旋和阻塞

  • 自旋: 当程序拿不到锁时,不放弃CPU,空转,不断重试
  • 阻塞:当程序拿不到锁时,放弃CPU,进入阻塞状态,等待后续被唤醒

就单核CPU而言,不能使用自旋,自旋会造成其他线程无法使用,对多核CPU而言,自旋可以减少线程切换的开销,节省资源

自旋和阻塞可以结合使用,先尝试自旋,若自旋几次还不能拿到锁,那就阻塞

2. AtomicBoolean和AtomicReference

在Unsafe类中,只提供了三种类型的CAS操作:int、long、Object(也就是引用类型)

2.1 如何实现 boolean

boolean通过int类型转换

3. AtomicStampedReference和 AtomicMarkableReference

3.1 ABA 问题及解决方案

  • 问题:如果另外一个线 程把变量的值从A改为B,再从B改回到A,那么尽管修改过两次,可是 在当前线程做CAS操作的时候,却会因为值没变而认为数据没有被其他线程修改过

  • 解决方案:要解决 ABA 问题,不仅要比较“值”,还要比较“版本号”

     public boolean compareAndSet(V   expectedReference,
                                     V   newReference,
                                     int expectedStamp,
                                     int newStamp) ;
    

3.2 为什么没有AtomicStampedInteger或 AtomictStampedLong

要同时比较数据的“值”和“版本号”,而Integer型或 者Long型的CAS没有办法同时比较两个变量,于是只能把值和版本号封装成一个对象,也就是这里面的 Pair 内部类,然后通过对象引用的CAS来实现。

private static class Pair<T> {
    final T reference;
    final int stamp;
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}

3.3 AtomicMarkableReference

AtomicMarkableReference与AtomicStampedReference原理类似, 只是Pair里面的版本号是boolean类型的,而不是整型的累加变量

3.4 为什么需要AtomicXXXFieldUpdater

如果一个类是自己编写的,则可以在编写的时候把成员变量定义 为Atomic类型。但如果是一个已经有的类,在不能更改其源代码的情 况下,要想实现对其成员变量的原子操作,就需要AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater。

  • 限制: 从代码层次可以看出,字段必须是volatile修饰,且不能是包装类

    if (field.getType() != int.class)
        throw new IllegalArgumentException("Must be integer type");
    
    if (!Modifier.isVolatile(modifiers))
        throw new IllegalArgumentException("Must be volatile type");
    

4. AtomicIntegerArray、AtomicLongArray和 Atomic-ReferenceArray

数组元素的原子操作,并不是说 对整个数组的操作是原子的,而是针对数组中一个元素的原子操作而言

 return unsafe.compareAndSwapInt(array, offset, expect, update);

5. Striped64与LongAdder

从JDK 8开始,针对Long型的原子操作,Java又提供了LongAdder、LongAccumulator;针对Double类型,Java提供了DoubleAdder、DoubleAccumulator

5. 1 LongAdder原理

AtomicLong内部是一个volatile long型变量,由多个线程对这个 变量进行CAS操作,多个线程同时对一个变量进行CAS操作,在高并发的场景下仍不够快。把一个变量拆成多份,变为多个变量,有些类似于 ConcurrentHashMap 的分段锁的例子,把一个Long型拆成一个base变 量外加多个Cell,每个Cell包装了一个Long型变量当多个线程并发 累加的时候,如果并发度低,就直接加到base变量上;如果并发度 高,冲突大,平摊到这些Cell上。在最后取值的时候,再把base和这些Cell求sum运算

由于无论是long,还是double,都是64位的。但因为没有double型的CAS操作,所以是通过把double型转化成long型来实现的。所以, 上面的base和cell[]变量,是位于基类Striped64当中的。英文Striped意为“条带”,也就是分片。

5.2 最终一致性

在sum求和函数中,并没有对cells[]数组加锁。也就是说,一边 有线程对其执行求和操作,一边还有线程修改数组里的值,也就是最 终一致性,而不是强一致性。这也类似于ConcurrentHashMap 中的 clear()函数,一边执行清空操作,一边还有线程放入数据,clear()函数调用完毕后再读取,hash map里面可能还有元素。因此,在LongAdder开篇的注释中,把它和AtomicLong 的使用场景做了比较。它 适合高并发的统计场景,而不适合要对某个 Long 型变量进行严格同步的场景

5.3 伪共享与缓存行填充

缓存 与主内存进行数据交换的基本单位叫Cache Line(缓存行)。在64位x86架构中,缓存行是64字节,也就是8个Long型的大小。这也意味着当缓存失效,要刷新到主内存的时候,最少要刷新64字节

主内存中有变量X、Y、Z(假设每个变量都是一个Long型),被CPU1和CPU2分别读入自己的缓存,放在了同一行Cache Line里面。当CPU1修改了X变量,它要失效整行Cache Line,也就是往总 线上发消息,通知CPU 2对应的Cache Line失效。由于Cache Line是数据交换的基本单位,无法只失效X,要失效就会失效整行的Cache Line,这会导致Y、Z变量的缓存也失效。

Y、Z和X变量处在了同一行Cache Line里面。要解 决这个问题,需要用到所谓的“缓存行填充”,分别在X、Y、Z后面加 上7个无用的Long型,填充整个缓存行,让X、Y、Z处在三行不同的缓存行中

posted @ 2021-07-17 10:46  雾里看花的少年  阅读(182)  评论(0编辑  收藏  举报