zoukankan      html  css  js  c++  java
  • 原子类

    原子类

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

  • 相关阅读:
    Spark&Hadoop:scala编写spark任务jar包,运行无法识别main函数,怎么办?
    Linux:krb5
    SqlServer数据库端口默认是1433吗?
    Linux下使用shell实现上传linux下某个目录下所有文件到ftp
    Spark+Hadoop+Hive集群上数据操作记录
    hadoop之 Hadoop2.2.0中HDFS的高可用性实现原理
    虚拟路由冗余协议VRRP
    hadoop 之Hadoop生态系统
    Oracle NoLogging Append 方式减少批量insert的redo_size
    Oracle常用的性能诊断语句
  • 原文地址:https://www.cnblogs.com/steven158/p/15022858.html
Copyright © 2011-2022 走看看