Atomic类(原子操作类)

1.AtomicInteger和AtomicLong

对于多线程操作一个数的加减,可以使用AtomicInteger代替synchronized,其性能更好。Atomic类皆为自旋性质。
源码:
public class AtomicInteger extends Number implements Serializable{
      private volatile int value;   //使用volatile修饰一个int变量,对其进行CAS操作。
      //自增方法
      public final int getAndIncrement(){
           for(;;){
                 int current = get();
                 int next = current + 1;
                 if(compareAndSet(current, next)){
                       return current;
                  }
            }
      }
 //自减方法
      public final int getAndIncrement(){
           for(;;){
                 int current = get();
                 int next = current - 1;
                 if(compareAndSet(current, next)){
                       return current;
                  }
            }
      }
 }
  • 悲观锁和乐观锁

    悲观锁:即认为数据发生冲突的概率很大,所以在读操作之前就上锁。synchronized和ReentrantLock都是悲观锁的典型例子。
    乐观锁:即认为数据发生冲突的概率比较小,读操作之前不上锁。
    写操作时,再判断数据在此期间是否被其它线程修改。被修改就读出来,未被修改则写回去。
    判断数据修改,同时写回新值,这两个操作要合成一个原子操作,也就是CAS(CompareAndSet)。
    
    AtomicInteger是典型的乐观锁实现。
    
  • Unsafe的CAS详解

    //原子操作类中的CAS方法调用的是Unsafe中的一个native函数:
    public final boolean compareAndSet(int expect, int update){
      return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    
    public final native boolean compareAndSwapInt(Object obj,long offset, int v1, int v2){};
    
    //Unsafe中成员变量转变为偏移量方法
    public native long objectFieldoffset(Field var1);
    
    compareAndSet方法中的第一个参数expect是指变量的旧值(是读出来的值,写回去的时候,期望不会被其它线程修改,所以称为expect),
    第二个参数update是指变量的新值(修改过的希望写入的值)。
    当expect等于变量当前的值时,说明在修改的期间,没有其它线程对此变量进行过修改,所以可以成功写入,变量更新为update,返回true,否则返回false。
    
    Unsafe中的所有方法都是native的。
    
    compareAndSwapInt方法有四个参数,其中,第一个是对象,第二个是对象的成员变量(即AtomicInteger里面包的int变量value)后面两个参数不变。
    
    其中第二个参数,经常被称为,XXXoffset,意思是某个成员变量在类中的内存偏移量(即变量在内存中的地址),表示该成员变量本身。
    所有调用CAS方法的地方,都会先通过objectFieldoffset方法将成员变量转变为一个偏移量。
    
  • 自旋与阻塞

    当一个线程拿不到锁的时候,有以下两种基本等待策略:
    1、放弃cpu,进入阻塞状态,等待后续被唤醒,再重新被操作系统调度
    2、不放弃cpu,空转,不断重试,即“自旋”
    
    AtomicInteger的实现使用的就是自旋,单核cpu只能使用策略1。synchronized使用的是策略1和2复合。
    

2.AtomicBoolean和AtomicDouble

  • 如何支持AtomicBoolean和AtomicDouble

    在Unsafe中只提供了三种类型的CAS操作:int,long,object(即引用类型)。
    
    AtomicBoolean使用int类型代替
    AtomicDouble依赖于double提供的一对double和long类型互转方法。
    

3.AtomicStamedReference和AtomicMarkableReference

  • ABA问题的解决

    ABA问题,即变量值被修改过,但修改后的值与原值相同,CAS会认为其没有被修改过。
    
    AtomicStamedReference的CAS方法:
    public final boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp);
    后两个参数是版本号的旧值和新值。
    
    当expectedReference != 当前对象的Reference时,说明被修改过
    当expectedReference == 当前对象的Reference时,再进一步比较expectedStamp是否等于当前的版本号。
    
  • AtomicMarkableReference

    AtomicMarkableReference与AtomicStamedReference原理相似,只是Pair内部类里的版本号是boolean类型的,而不是整型的累加变量。因为是boolean类型,所以只有true和false两种状态,不能完全避免ABA问题,只能减少其发生的概率。
    

4.AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater

  • 为什么需要Atomic***FieldUpdater

    对一个非自己编写的,已有的类要想实现对其成员变量的原子操作,就需要Atomic***FieldUpdater。
    
  • 限制条件

    要想使用Atomic***FieldUpdater,则必须保证成员变量是volatile关键字修饰的
    

5.AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray

注意:这三个类并不是对整个数组进行原子操作,而是针对数组中的一个元素。

其底层还是使用的compareAndSwapInt方法

6.Striped和LongAdder

jdk8开始,针对Long型的原子操作,java又提供了LongAddr、Long Accumulator,针对Double,java提供了DoubleAddr、DoubleAccumulator
  • LongAddr原理

    AtomicLong内部是一个volatile long型变量,在高并发场景下依旧不够快。
    
    把一个变量拆成多份,变为多个变量。即把一个Long型,拆分为一个base变量外加多个Cell,每个Cell包装一个Long型。当多个变量进行并发累加时,如果并发度低,则直接加到base上,如果并发度高,则平摊到这些Cell上。最后取值,使Cell和base进行sun操作即可。
    
    在sum中并没有对cells[]数组进行加锁,即不保证强一致性,只保证最终一致性。
    
    所以,LongAddr适合高并发的统计场景,而不适合要对某个long型进行严格同步的场景。
    
  • 伪共享与缓存填充

    变量在同一个缓存行(cache line)中,缓存一致性协议,cpu1其中一个变量x修改,会导致cpu2中的同一个变量x失效,该缓存行失效,从而导致该缓存行中的其它未被修改的两cpu共享的变量失效,导致伪共享问题。
    
    原因是x,y,z处在了同一个缓存行中。
    
    解决:进行缓存行填充,即一个变量一个缓存行,在64bit的机器上一个缓存行是64byte,即假设声明一个long,还需要填充7个long类型的无用变量,进行缓存行填充,是为了不让cell[]数组中相邻的元素落到同一个缓存行里。
    jdk8提供了@sun.misc.Contended声明进行填充缓存行。
    
  • LongAdder核心实现

    最核心的实现是add方法。当一个线程调用add方法的时候,会首先尝试加在base变量上,不成功则分配到cell元素,再不成功则会调用longAccumulate方法
    
  • LongAccumulator

    LongAdder只能进行累加操作,并且初始值默认为0,LongAccumulator可以自己定义一个二元操作符。
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容