java原子类-Atomic

什么是原子类?

java 1.5引进原子类,具体在java.util.concurrent.atomic包下,atomic包里面一共提供了13个类,分为4种类型,分别是:

  • 原子更新基本类型
  • 原子更新数组
  • 原子更新引用
  • 原子更新属性。

原子类也是java实现同步的一套解决方案。

什么时候使用原子类?

当我们只是需要一个简单的、高效、线程安全的递增或者递减方案:

  1. 简单:操作简单,底层实现j简单
  2. 高效:占用资源少,操作速度快
  3. 安全:在高并发和多线程环境下要保证数据的正确性

当然这种情况可以使用synchronized关键字和lock可以实现,但是代码量会上去,而且性能也会降低一点,所以用原子类就比较方便一点

原子变量类简单介绍

原子变量类在java.util.concurrent.atomic包下,总体来看有这么多个:

基本类型:

  • AtomicBoolean:布尔型
  • AtomicInteger:整型
  • AtomicLong:长整型

数组:

  • AtomicIntegerArray:数组里的整型
  • AtomicLongArray:数组里的长整型
  • AtomicReferenceArray:数组里的引用类型

引用类型:

  • AtomicReference:引用类型
  • AtomicStampedReference:带有版本号的引用类型
  • AtomicMarkableReference:带有标记位的引用类型

对象的属性:

  • AtomicIntegerFieldUpdater:原子更新对象中int类型字段的值,基于反射的使用程序,可以对指定类的指定的volatile int字段进行原子更新
  • AtomicLongFieldUpdater:原子更新对象中Long类型字段的值,基于反射的使用程序,可以对指定类的指定的volatile long字段进行原子更新
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的值,基于反射的使用程序,可以对指定类的指定的volatile volatile引用进行原子更新

JDK8新增

Accumulator累加器

  • DoubleAccumulator、
  • LongAccumulator、

Adder累加器

  • DoubleAdder
  • LongAdder

是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。

Atomic包里的类基本都是使用Unsafe实现的包装类。 从原理上来说就是:Atomic包的类的实现大多数都是调用的unsafe方法,而unsafe底层实际上是调用C代码,C代码调用汇编,最后生成出一条CPU指令cmpxchg,完成操作,这也是为什么CAS是原子性操作,因为是一条CPU指令,不会被打断。

原子变量类的使用

基本类型原子类(Atomic*)

这里以原子更新基本类型中的AtomicInteger类为例,介绍通用的API接口和使用方法。

常用的API:

public final int set() //设一个值
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果当前值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue) //最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
    AtomicInteger a = new AtomicInteger(0);
        for (int i = 1; i < 5; i++) {
            a.getAndIncrement(); // a 自增,相当于 a ++
        }
        //获取当前a的值
        System.out.println("AtomicInteger a从0自增4次结果为:"+ a.get());
​
        System.out.println("AtomicInteger 当前a为:"+ a.getAndDecrement() + ",并自减一次"); //a --
​
        //获取当前a的值,并更新a为8
        System.out.println("AtomicInteger a当前值为:"+a.getAndSet(8)+",并更新a为8");
​
        //获取当前a的值,并将a加6
        System.out.println("AtomicInteger a当前值为:"+a.getAndAdd(6)+",并将a加6");
​
        a.compareAndSet(12,9); //如果a=12,就把a更新为9,否则不进行操作
        System.out.println("AtomicInteger a当前值为:"+a.get());
​
        a.compareAndSet(14,9); //如果a=14,就把a更新为9,否则不进行操作
        System.out.println("AtomicInteger a当前值为:"+a.get());
​

AtomicInteger a从0自增4次结果为:4 AtomicInteger 当前a为:4,并自减一次 AtomicInteger a当前值为:3,并更新a为8 AtomicInteger a当前值为:8,并将a加6 AtomicInteger a当前值为:14 AtomicInteger a当前值为:9

数组类型原子类(Atomic*Array)

这里以AtomicIntegerArray 为例,介绍通用的API接口和使用方法。

常用API:

public final int get(int i) //获取 index=i 位置元素的值
public final int set(int i, int newValue) //为 index=i 位置元素设新值
public final int getAndSet(int i, int newValue) //返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果index=i 位置的值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue) //最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

基本使用:

         int[] a = {1,1,1,1};
        AtomicIntegerArray arr = new AtomicIntegerArray(a);
        System.out.println("arr数组初始值为:" +arr.toString());
​
        for (int i = 0; i < 4; i++) {
            arr.getAndIncrement(i); //index = i位置上的值arr[i]自增,相当于 a[i] ++
        }
        System.out.println("arr数组每个元素都自增1后为:" +arr.toString());
        //获取当前arr[1]的值
        System.out.println("arr[1]的值为:"+ arr.get(1));
​
        System.out.println("arr[2]当前值为:"+ arr.getAndDecrement(2) + ",并让arr[2]自减一次"); //a[2]--
​
        //获取当前a[2]的值,并更新a为8
        System.out.println("arr[2]当前值为:"+arr.getAndSet(2,8)+",并更新a[2]为8");
​
        //获取当前a的值,并将a加6
        System.out.println("arr[2]当前值为:"+arr.getAndAdd(2,6)+",并将a[2]加6");
​
        arr.compareAndSet(2,12,9); //如果a[2]=12,就把a[2]更新为9,否则不进行操作
        System.out.println("arr[2]当前值为:"+arr.get(2));
​
        arr.compareAndSet(2,14,9); //如果a[2]=14,就把a[2]更新为9,否则不进行操作
        System.out.println("arr[2]当前值为:"+arr.get(2));

arr数组初始值为:[1, 1, 1, 1] arr数组每个元素都自增1后为:[2, 2, 2, 2] arr[1]的值为:2 arr[2]当前值为:2,并让arr[2]自减一次 arr[2]当前值为:1,并更新a[2]为8 arr[2]当前值为:8,并将a[2]加6 arr[2]当前值为:14 arr[2]当前值为:9

引用类型原子类(Atomic*Reference)

User类:

package 原子类;
​
public class User {
        private String name;
        private int age;
​
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
​
        public String getName() {
            return this.name;
        }
​
        public void setName(final String name) {
            this.name = name;
        }
​
        public int getAge() {
            return this.age;
        }
​
        public void setAge(final int age) {
            this.age = age;
        }
​
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + ''' +
                    ", age=" + age +
                    '}';
    }
​
}

 package 原子类;


import java.util.concurrent.atomic.AtomicReference;
​
public class Demo3 {
​
    public static void main(String[] args) {
        AtomicReference atu=new AtomicReference<>();
​
        User user1=new User("张三",10);
        User user2=new User("李四",16);
        User user3=new User("王五",19);
        atu.set(user1);
        //常用的API和前面的是差不多的
        System.out.println(atu.getAndSet(user2));
        System.out.println(atu.get());
​
        System.out.println(atu.compareAndSet(user2,user3));
        System.out.println(atu.get());
    }
//User{name='张三', age=10}
//User{name='李四', age=16}
//true
//User{name='王五', age=19}
​
​
}
什么是ABA问题?

ABA问题指在CAS操作过程中,当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。这时候就可能导致程序出现意外的结果。

在高并发场景下,使用CAS操作可能存在ABA问题,也就是在一个值被修改之前,先被其他线程修改为另外的值,然后再被修改回原值,此时CAS操作会认为这个值没有被修改过,导致数据不一致。

如何解决?

为了解决ABA问题,Java中提供了AtomicStampedReference类(原子标记参考),该类通过使用版本号的方式来解决ABA问题。每个共享变量都会关联一个版本号,CAS操作时需要同时检查值和版本号是否匹配。因此,如果共享变量的值被改变了,版本号也会发生变化,即使共享变量被改回原来的值,版本号也不同,因此CAS操作会失败。

AtomicStampedReference:解决ABA问题

带版本号的引用类型原子类,可以解决ABA问题

AtomicStampedReference在构建的时候需要一个类似于版本号的int类型变量stamped,每一次针对共享数据的变化都会导致该 stamped 的变化(stamped 需要应用程序自身去负责,AtomicStampedReference并不提供,一般使用时间戳作为版本号),因此就可以避免ABA问题的出现,AtomicStampedReference的使用也是极其简单的,创建时我们不仅需要指定初始值,还需要设定stamped的初始值,在AtomicStampedReference内部会将这两个变量封装成Pair对象

package 原子类;
import 原子类.User;
import java.util.concurrent.atomic.AtomicStampedReference;
class ABADemo {
    public static void main(String[] args) {
        User zs = new User("张三",18);
        User ls = new User("李四",25);
        //创建对象带版本号的引用类型原子类,添加对象和初始化的版本号
        AtomicStampedReference reference = new AtomicStampedReference<>(zs, 1);
​
        new Thread(() -> {
            int stamp = reference.getStamp();
            User referenceUser = reference.getReference();
            // 保证线程t2可以拿到版本号
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // A
            System.out.println(Thread.currentThread().getName() + "版本号:" + stamp + " 对象" + referenceUser);
            // B
            boolean compareAndSet = reference.compareAndSet(referenceUser, ls, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "将数据设置" + (compareAndSet?"成功 ":"失败 ") + reference.getReference());
            // A
            compareAndSet = reference.compareAndSet(reference.getReference(),zs,reference.getStamp(),reference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "将数据设置" + (compareAndSet?"成功 ":"失败 ") + reference.getReference());
​
        },"t1").start();
​
        new Thread(() -> {
            int stamp = reference.getStamp();
            User referenceUser = reference.getReference();
            // 保证线程t1发生完ABA问题
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean compareAndSet = reference.compareAndSet(referenceUser,ls,stamp,stamp + 1);
            System.out.println(Thread.currentThread().getName() + "将数据设置" + (compareAndSet?"成功 ":"失败 ") + reference.getReference());
​
        },"t2").start();
    }
}

t1版本号:1 对象User{name='张三', age=18} t1将数据设置成功 User{name='李四', age=25} t1将数据设置成功 User{name='张三', age=18} t2将数据设置失败 User{name='张三', age=18}

常用API:

// 构造函数,初始化引用和版本号
public AtomicStampedReference(V initialRef, int initialStamp)
 
// 以原子方式获取当前引用值
public V getReference()
 
// 以原子方式获取当前版本号
public int getStamp()
 
// 以原子方式获取当前引用值和版本号
public V get(int[] stampHolder)
 
// 以原子的方式同时更新引用值和版本号
// 当期望引用值不等于当前引用值时,操作失败,返回false
// 当期望版本号不等于当前版本号时,操作失败,返回false
// 在期望引用值和期望版本号同时等于当前值的前提下
// 当新的引用值和新的版本号同时等于当前值时,不更新,直接返回true
// 当新的引用值和新的版本号不同时等于当前值时,同时设置新的引用值和新的版本号,返回true
public boolean weakCompareAndSet(V  expectedReference,
                                 V  newReference,
                                 int expectedStamp,
                                 int newStamp)
 
// 以原子的方式同时更新引用值和版本号
// 当期望引用值不等于当前引用值时,操作失败,返回false
// 当期望版本号不等于当前版本号时,操作失败,返回false
// 在期望引用值和期望版本号同时等于当前值的前提下
// 当新的引用值和新的版本号同时等于当前值时,不更新,直接返回true
// 当新的引用值和新的版本号不同时等于当前值时,同时设置新的引用值和新的版本号,返回true
public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp)
 
// 以原子方式设置引用的当前值为新值newReference
// 同时,以原子方式设置版本号的当前值为新值newStamp
// 新引用值和新版本号只要有一个跟当前值不一样,就进行更新
public void set(V newReference, int newStamp)
 
// 以原子方式设置版本号为新的值
// 前提:引用值保持不变
// 当期望的引用值与当前引用值不相同时,操作失败,返回fasle
// 当期望的引用值与当前引用值相同时,操作成功,返回true
public boolean attemptStamp(V expectedReference, int newStamp)
 
// 使用`sun.misc.Unsafe`类原子地交换两个对象
private boolean casPair(Pair cmp, Pair val)
​
AtomicMarkableReference 状态戳简化

AtomicMarkableReference与AtomicStampedReference的区别是Pair内部类维护的类型不同。

类似于上面的版本号,但是主要是解决一次性问题

解决是否修改过,它的定义就是将状态戳简化boolean也就是true或者falset,类似于一次性筷子

    static AtomicMarkableReference markableReference = new AtomicMarkableReference<>(100,false);
    public static void main(String[] args) {
        new Thread(()->{
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t"+"默认标识"+marked);
            //暂停1秒钟线程,等待后面的T2线程和我拿到一样的模式flag标识,都是false
            try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
            markableReference.compareAndSet(100, 1000, marked, !marked);
        },"t1").start();
 
        new Thread(()->{
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t"+"默认标识"+marked);
            //这里停2秒,让t1先修改,然后t2试着修改
            try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
            boolean t2Result = markableReference.compareAndSet(100, 1000, marked, !marked);
            System.out.println(Thread.currentThread().getName()+"\t"+"t2线程result--"+t2Result);
            System.out.println(Thread.currentThread().getName()+"\t"+markableReference.isMarked());
            System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference());
 
        },"t2").start();
    }
}
​

运行结果:

//t1 默认标识false //t2 默认标识false //t2 t2线程result--false //t2 true //t2 1000

对象的属性修改原子类

使用要求:

  • 更新的对象属性必须使用public volatile修饰符
  • 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
  • 属性的修饰符(public/protected/default/private)要保证当前操作对该属性可以直接进行,比如当我们用private volatile int age 时就会报错,因为private修饰时,外部无法访问也无法修改。
  • 只能是实例变量,不能是类变量,也就是说不能加static关键字。
  • 只能是可修改变量,不能使final变量,因为final的语义就是不可修改。
  • 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段不能修改其包装类型(Integer/Long) 。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。

修改引用类型:

package 原子类;
​
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
​
class yingyongDemo {
    public static void main(String[] args) {
        MyVar myVar = new MyVar();
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                myVar.init();
            }, String.valueOf(i)).start();
​
        }
    }
​
    public static class MyVar {
        public volatile Boolean isInit = Boolean.FALSE;
        AtomicReferenceFieldUpdater referenceFieldUpdater =
                AtomicReferenceFieldUpdater.newUpdater(MyVar.class, Boolean.class, "isInit");
​
        public void init() {
            if (referenceFieldUpdater.compareAndSet(this, Boolean.FALSE, Boolean.TRUE)) {
                System.out.println(Thread.currentThread().getName() + "\t" + "-----start init,needs 3 seconds");
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + "-----over init");
            } else {
                System.out.println(Thread.currentThread().getName() + "\t" + "抱歉,已经有其他线程进行了初始化");
            }
        }
    }
}

1 -----start init,needs 3 seconds 5 抱歉,已经有其他线程进行了初始化 4 抱歉,已经有其他线程进行了初始化 2 抱歉,已经有其他线程进行了初始化 3 抱歉,已经有其他线程进行了初始化 1 -----over init

Adder累加器

LogAdder比AtomicLog的区别

  • java8引入的,相比较是一个比较新的类

  • 高并发下LogAdder比AtomicLog效率高,不过本质是空间换时间

  • 竞争激烈的时候,LongAdder把不同线程对应到不同的Cell上进行修改,降低了冲突的概率,是多段锁的理念,提高了并发性

  • LongAdder适合的场景是统计求和计数的场景,而且LongAdder基本只提供了add方法,而AtomicLong还具有cas方法

    多线程下AtomicLong的性能,有20个线程对同一个AtomicLong累加(由于竞争很激烈,每一次加法,都要flush和refresh,导致很耗费资源)java原子类-Atomic_第1张图片

  • 在内部,这个LongAdder的实现原理和AtomicLong是不同的,刚才的AtomicLong的实现原理是,每一次加法都需要做同步,所以在高并发的时候会导致冲突比较多,也就降低了效率

  • 而此时的LongAdder,每个线程都有一个计数器,仅用来在自己的线程内计算,这样一来就不会和其他线程的计数器干扰

  • 如下图,第一个线程的计数器的数值,也就是ctr’,为1的时候,可能线程2的计数器ctr’’的数值已经是3了,他们之间并不存在竞争关系,所以在加和的过程中,根本不需要同步机制,也不需要刚才的flush和reflush。这里没有一个公共的counter来给所有线程统一计数

  • LongAdder引入了分段累加的概念,内部有一个base变量和一个Cell[]数组共同参与计数:

    base变量:竞争不激烈,直接累加到该变量上

    Cell[]:竞争激烈,各个线程分散累加到自己的槽Cell[i]中,java原子类-Atomic_第2张图片

总的来说: LongAdder的基本思路就是分散热点 ,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。sum源码:

 public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }
​

sum()会将所有Cell数组中的value和base累加作为返回值, 核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点 。java原子类-Atomic_第3张图片

一句话总结longAdder原理

LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组 ,将一个value拆分进这个数组cells。 多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。 与AtomicLong对比

名称 原理 场景 缺陷
AtomicLong CAS + 自旋 低并发下的全局计算,AlomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。可允许一些性能损耗,要求高精度时可使用。AtomicLong是多个线程针对单个热点值value进行原子操作 高并发后性能急剧下降。(N个线程CAS操作修改线程的值,每次只有一个成功过,其它N-1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,占用大量CPU)
LongAdder CAS+Base+Cell数组分散,通过空间换时间分散了热点数据 高并发下的全局计算,当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用。LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作 sum求和后还有计算线程修改结果的话,最后结果不够准确

Accumulator累加器

public class LongAccumulatorDemo {
    public static void main(String[] args) {
        //需要传入累加的函数
        LongAccumulator accumulator = new LongAccumulator((x,y)->x+y,0);
        ExecutorService executor = Executors.newFixedThreadPool(8);
        IntStream.range(1,10).forEach(i->executor.submit(()->accumulator.accumulate(i)));
        executor.shutdown();
        while (!executor.isTerminated()){
​
        }
        System.out.println(accumulator.getThenReset()); //45
    }
}
​

 

使用场景:

① 适用于需要大量计算,并且需要并行计算的场景,如果不需要并行计算,可用for循环解决问题,用了Accumulator累加器可利用多核同时计算,提供效率

② 计算的顺序不能成为瓶颈,线程1可能在线程5之后运行,也可能在之前运行,不影响最终结果

你可能感兴趣的:(java,开发语言,算法,后端)