ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

原子类

2021-07-17 11:03:34  阅读:137  来源: 互联网

标签:缓存 变量 int Long 原子 CAS 线程


原子类

阿里巴巴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处在三行不同的缓存行中

标签:缓存,变量,int,Long,原子,CAS,线程
来源: https://www.cnblogs.com/steven158/p/15022858.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有