java原子类详解

7 篇文章 0 订阅
5 篇文章 0 订阅

java原子类详解

什么原子类

原子类是具有原子性的类,原子性的意思是对于一组操作,要么全部执行成功,要么全部执行失败,不能只有其中某几个执行成功。

原子类作用

作用和锁有类似之处,是为了保证并发情况下的线程安全。

相对于锁的优势

  • 粒度更细
    原子变量可以把竞争范围缩小到变量级别,通常情况下锁的粒度也大于原子变量的粒度

  • 效率更高
    除了在高并发之外,使用原子类的效率往往比使用同步互斥锁的效率更高,因为原子类底层利用了CAS,不会阻塞线程。

原子类种类

在JDK中J.U.C包下提供了种类丰富的原子类,以下所示:

类型具体类型
Atomic* 基本类型原子类AtomicInteger、AtomicLong、AtomicBoolean
Atomic*Array 数组类型原子类AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
Atomic*Reference 引用类型原子类AtomicReference、AtomicStampedReference、AtomicMarkableReference
Atomic*FieldUpdater 升级类型原子类AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
Adder 累加器LongAdder、DoubleAdder
Accumulator 积累器LongAccumulator、DoubleAccumulator

AtomicInteger常用方法

上面列举了J.U.C中提供的一些原子操作类,接下来从简单的AtomicInteger开始分析,来看看它的常用方法,其他的两种AtomicLong、AtomicBoolean和它相似

方法作用
public final int get()获取当前的值
public final int getAndSet(int newValue)获取当前的值,并设置新的值
public final int getAndIncrement() 获取当前的值,并自增+1
public final int getAndDecrement() 获取当前的值,并自减-1
public final int getAndAdd(int delta) 获取当前的值,并加上预期的值。getAndIncrement和getAndDecrement不满足,可使用当前方法
boolean compareAndSet(int expect, int update) 如果输入的数值等于预期值,则以原子方式将该值更新为输入值(update)

Array 数组类型原子类

AtomicArray 数组类型原子类,数组里的元素,都可以保证其原子性,比如 AtomicIntegerArray 相当于把 AtomicInteger 聚合起来,组合成一个数组。我们如果想用一个每一个元素都具备原子性的数组的话, 就可以使用 AtomicArray

该类包括:

类名作用
AtomicIntegerArray整形数组原子类
AtomicLongArray长整形数组原子类
AtomicReferenceArray引用类型数组原子类

Atomic*Reference 引用类型原子类

AtomicReference引用类型原子类,作用和AtomicInteger没有本质区别,AtomicReference是让一个对象保持原子性,而不局限一个变量。

该种类所有类型:

类名作用
AtomicReference保证对象的原子性
AtomicStampedReference它是对 AtomicReference 的升级,在此基础上还加了时间戳,用于解决 CAS 的 ABA 问题。
AtomicMarkableReference和 AtomicReference 类似,多了一个绑定的布尔值,可以用于表示该对象已删除等场景。

Atomic*FieldUpdater原子更新器

原子类更新器主要用于对已经声明的非原子变量,为它增加原子性,让该变量拥有CAS操作的能力。
该种类所有类型:

类名作用
AtomicIntegerFieldUpdater原子更新整形的更新器
AtomicLongFieldUpdater原子更新长整形的更新器
AtomicReferenceFieldUpdater原子更新引用的更新器

这里使用AtomicIntegerFieldUpdater来举例说明:

public class AtomicIntegerFieldUpdaterDemo implements Runnable {

    static Score useUpdaterScore;
    static Score unusedComputerScore;

    //声明原子更新类,泛型为Score类,更新的是"score"字段
    public static AtomicIntegerFieldUpdater<Score> scoreUpdater = AtomicIntegerFieldUpdater
            .newUpdater(Score.class, "score");

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            //非原子操作直接自增
            unusedComputerScore.score++;
            //使用更新器更新字段,同样执行自增
            scoreUpdater.getAndIncrement(useUpdaterScore);
        }
    }

    public static class Score {
        volatile int score;
    }

    public static void main(String[] args) throws InterruptedException {
        useUpdaterScore = new Score();
        unusedComputerScore = new Score();
        AtomicIntegerFieldUpdaterDemo r = new AtomicIntegerFieldUpdaterDemo();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("普通变量的结果:" + unusedComputerScore.score);
        System.out.println("升级后的结果:" + useUpdaterScore.score);
    }
}

输出结果:
普通变量的结果:1980
升级后的结果:2000

从结果可以看出,升级后结果是期望值。

  • 使用场景(相对于直接使用AtomicInteger)
    • 历史原因
      如果当前变量在之前开发版本中使用地方过多,这个时候为了不做大量改造,可以在需要原子性的时候,使用原子更新器
    • 少部分情况需要原子性的时候
      由于原子类型的变量比普通变量更耗资源。在大部分不需要原子性的时候,如果都设置成原子类型,这非常的耗资源。因此,我们可以再需要原子性的少数情况下,对当前变量使用AtomicIntegerFieldUpdater进行合理的升级。

常见问题

原子类是如何利用 CAS 保证线程安全的?

以AtomicInteger为例,jdk 8实现,看怎么实现累加原子操作的

public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

U是Unsafe类

该方法实际调用的是,unsafe.getAndAddInt(this, VALUE, delta),这里先介绍一下Unsafe类。

  • Unsafe类
    Unsafe是CAS的核心类。由于java无法直接访问底层操作系统,而是需要通过本地方法来实现。JVM还是提供了Unsafe类,他提供了硬件层面的原子操作,可以直接操作内存的数据。

接下来看一下AtomicInteger的一些关键代码

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    private volatile int value;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception var1) {
            throw new Error(var1);
        }
    }

    private volatile int value;

    public final int get() {
        return value;
    }
    ......
}

首先获取Unsafe类型变量unsafe,并定义变量valueOffset,然后在静态代码块中value值在内存中的偏移地址,赋值给valueOffset变量。因为 Unsafe 就是根据内存偏移地址获取数据的原值的,这样我们就能通过 Unsafe 来实现 CAS 了。

value是用volatile修饰的,他就是我们原子类存储值的变量,由于使用volatile修饰,所以在多线程中看到的value值都是同一份,保证了可见性。

接下来看实际执行cas的地方。Unsafe的getAndAddInt()方法

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;
}

先看do-while循环,它是一个无限循环,知道满足条件才退出循环。do中的代码时,获取到当前内存中的值赋值给var5,var1是当前原子对象,var2是value在内存中的偏移量。此时var5就是此时原子类的数值。

再看看while中的退出条件。compareAndSwapInt这个方法参数,他们的实际意义是:

  • 第一个参数 --> 当前原子类对象,即AtomicInteger 这个对象本身
  • 第二个参数 --> 当前值的内存偏移量,接触它可以获取到value的值
  • 第三个参数 --> 当前值,如果当前值不匹配,更新失败会返回false,开始下次循环。如果当前值匹配,同时更新成功,方法返回true,跳出循环,完成当前累加操作
  • 第四个参数 --> 期望值,即当前值加上累加的值。

所以 compareAndSwapInt 方法的作用就是,判断如果现在原子类里 value 的值和之前获取到的 var5 相等的话,那么就把计算出来的 var5 + var4 给更新上去,所以说这行代码就实现了 CAS 的过程。

总结一下,Unsafe 的 getAndAddInt 方法是通过循环 + CAS 的方式来实现的,在此过程中,它会通过 compareAndSwapInt 方法来尝试更新 value 的值,如果更新失败就重新获取,然后再次尝试更新,直到更新成功。

AtomicInteger 在高并发下

AtomicLong 高并发下存在的问题

在并发情况下,如果我们需要实现计数器(例如下载任务数),可以利用AtomicInteger和AtomicLong,这样一来可以避免加锁和复杂的代码逻辑。虽然它们好用,但是也存在着一些问题。

public class AtomicLongDemo {
 
   public static void main(String[] args) throws InterruptedException {
       AtomicLong counter = new AtomicLong(0);
       ExecutorService service = Executors.newFixedThreadPool(16);
       for (int i = 0; i < 100; i++) {
           service.submit(new Task(counter));
       }
 
       Thread.sleep(2000);
       System.out.println(counter.get());
   }
 
   static class Task implements Runnable {
 
       private final AtomicLong counter;
 
       public Task(AtomicLong counter) {
           this.counter = counter;
       }
 
       @Override
       public void run() {
           counter.incrementAndGet();
       }
   }
}

在这里,定义了一个为0的AtomicLong变量counter,然后创建了线程池数为16的线程池。然后执行100个任务。然后任务是执行counter.incrementAndGet()。结果毫无疑问是100,虽然并发执行,但是AtomicLong依然能保证incrementAndGet是一个原子操作,所以不会发生线程安全问题。

不过我们仔细看细节,对于 AtomicLong 内部的 value 属性而言,也就是保存当前 AtomicLong 数值的属性,它是被 volatile 修饰的,所以它需要保证自身可见性。
当线程1执行incrementAndGet操作更新成功后,会将值向主内存中修改,同时主内存会将其他线程的value给修改掉,而且CAS也会经常失败,这两个操作是非常耗资源的。

如何改进AtomicLong 在高并发下性能

在JDK 8中新增了LongAdder 类,来优化这一个问题。

我们将上面例子中做小小的调整,将AtomicLong换成LongAdder,get()方法换成sum(),incrementAndGet()换成increment()

public class LongAdderDemo {
 
   public static void main(String[] args) throws InterruptedException {
       LongAdder counter = new LongAdder();
       ExecutorService service = Executors.newFixedThreadPool(16);
       for (int i = 0; i < 100; i++) {
           service.submit(new Task(counter));
       }
 
       Thread.sleep(2000);
       System.out.println(counter.sum());
   }
   static class Task implements Runnable {
 
       private final LongAdder counter;
 
       public Task(LongAdder counter) {
           this.counter = counter;
       }
 
       @Override
       public void run() {
           counter.increment();
       }
   }
}

运行得到的结果还是100,但是运行速度比刚才AtomicLong实现要快。那么为什么LongAddr比AtomicLong快呢?

LongAddr源码解析
//集成自Striped64
public class LongAdder extends Striped64 implements Serializable {}


abstract class Striped64 extends Number {
    ......
     /**
     * 单元格表。如果为非null,则大小为2的幂。
     */
    transient volatile Cell[] cells;

    /**
     * 基本值,主要在没有争用时使用,也用作表初始化过程中的回退。通过CAS更新。
     */
    transient volatile long base;

    ......
}

LongAdder中引入了分段累加的概念,内部的Cell[]数组和base变量都参与了计数

其中base在竞争不激烈的情况下,直接把累加的结果改到base变量上;
在竞争激烈的时候,各个线程会分散累加到自己所对应的Cell[] 数组的某一个对象中,而不是公用一个。LongAdder这样分段的思想将不同线程到不同Cell上的修改,避免了大量的冲突,提升了并发性。更JDK 7 中的ConcurrentHashMap思想相似。

竞争激烈的时候,LongAdder 会通过计算出每个线程的 hash 值来给线程分配到不同的 Cell 上去,每个 Cell 相当于是一个独立的计数器,这样一来就不会和其他的计数器干扰,Cell 之间并不存在竞争关系,所以在自加的过程中降低了冲突的概率。这个思想的本质就是空间换时间,所以会耗费更多的内存。

最终的结果是通过LongAdder#sum()方法来获取的,将各个Cell值累计求和,再加上base返回。

public long sum() {
   Cell[] as = cells;
   long sum = base;
   if (as != null) {
       for (Cell a : as)
           if (a != null)
               sum += a.value;
   }
   return sum;
}

AtomicLong和LongAdder使用如何选择

在低竞争的情况下,AtomicLong和LongAdder的性能相似;但是在竞争激烈的情况下,LongAdder 的预期吞吐量要高得多,经过试验,LongAdder 的吞吐量大约是 AtomicLong 的十倍,虽然性能提高了,但是LongAdder会耗费更多的空间

  • LongAdder 只提供了 add、increment 等简单的方法,适合的是统计求和计数的场景,场景比较单一
  • 而 AtomicLong 还具有 compareAndSet 等高级方法,可以应对除了加减之外的更复杂的需要 CAS 的场景。

  • 小结
    如果我们的场景仅仅是需要用到加和减操作的话,那么可以直接使用更高效的 LongAdder,但如果我们需要利用 CAS 比如 compareAndSet 等操作的话,就需要使用 AtomicLong 来完成。

原子类 和 volatile的区别

  • volatile只保证可见性
    volatile只保证了可见性,而不能保证原子性。例如主内存中volatile int a=0,在两个线程中时,如果都同时获取当前的volatile修饰的变量值,但是都执行自增操作,由于两个线程都在之前获取了值即a=0,两线程都执行自增后都是a=1了,这是两个线程同时更新主内存中的a,最后得到的结果是a=1,这个就和预期值不一样,由于并没有保证获取和赋值这个操作的原子性,会有线程安全问题。

  • 原子类保证了可见性和原子性
    上面的例子可以使用原子类AtomicInteger来修复,将自增操作换成incrementAndGet(),底层通过CPU指令保证原子性,解决线程安全问题

使用场景
-使用场景
volatile通常情况下,volatile 可以用来修饰 boolean 类型的标记位,因为对于标记位来讲,直接的赋值操作本身就是具备原子性的,再加上 volatile 保证了可见性,那么就是线程安全的了
原子类对于先获取值,然后做一定修改,再赋值回去的操作,就需要原子类来保证原子性

AtomicInteger 和 synchronized 的异同点

相同点
  • 都能保证线程安全
不同点
-原理使用范围粒度性能
AtomicInteger使用CAS来保证线程的原子性仅一个对象,少数场景使用竞争在变量级别乐观锁,低并发时性能好
synchronized使用monitor锁来保证线程安全性。再执行同步代码之前,需要先获取到monitor锁,执行完毕,释放锁。既可以修饰一个方法,又可以修饰一段代码,可以根据我们的需要,非常灵活地去控制它的应用范围通常会大于变量级别悲观锁(jdk6之后锁升级优化后,低并发情况性能也不错),在高并发时由于AtomicInteger

Java 8 中 Adder 和 Accumulator 有什么区别

上面讲了Adder是通过CAS加分段思想来提高Atomic*的性能。

而LongAccumulator是LongAdder的功能增强版,LongAccumulator在LongAdder只有数值加减的基础上提供自定义的函数操作。

例如,下面代码

public class LongAccumulatorDemo {

    public static void main(String[] args) throws InterruptedException {
        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)));

        Thread.sleep(2000);
        System.out.println(accumulator.getThenReset());
    }
}

上面是使用8线程数的线程池累加0-9。最后获取到结果为45.

x是上一次的结果,y是传入的新值。由于使用多线程,当前的方法只适用于即使执行顺序不同,结果依然一样的情况,即 交 换 性 \color{red}交换性

下面几种场景适合使用:

  • 相加
  • 相乘
  • 最大值、最小值

总结

上面讲了原子类所有的种类、使用、及其原理,同时也介绍了各个原子类的使用场景,并且和volatile、synchronized做了对比。我们在实际开发中可以根据具体业务来选择适合的原子类使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值