当前位置:   article > 正文

JUC Atomic:原子类解析_juc 原子类

juc 原子类

JUC整体框架

一、JUC 包中的原子类是哪 4 类?

基本类型

使用原子的方式更新基本类型

  • AtomicInteger:整形原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
  • AtomicMarkableReference :原子更新带有标记位的引用类型

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

二、CAS详解

JUC中多数类是通过volatile和CAS来实现的,CAS本质上提供的是一种无锁方案,而Synchronized和Lock是互斥锁方案; java原子类本质上使用的是CAS,而CAS底层是通过Unsafe类实现的。所以需要对CAS, Unsafe和原子类详解

1、CAS的概念

CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。

简单解释就是:
真实值和期望值相同,就修改成功,真实值和期望值不同,就修改失败!

AtomicInteger为例去理解CAS的概念:

AtomicInteger atomicInteger = new AtomicInteger(2020);  // 旧值
boolean ato = atomicInteger.compareAndSet(2020, 2021);  // 分别为期望值和新值,ato为true
System.out.println(atomicInteger.get());  // 结果为:2021
  • 1
  • 2
  • 3

public final boolean compareAndSet(int expect, int update) // 如果我期望值的达到了,那么就更新,否则,就不更新(会一直循环,一直往下看源码可知),CAS是CPU的并发原语

2、CAS的好处

CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁,JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。

如果不使用CAS,在高并发下,多线程同时修改一个变量的值我们需要synchronized加锁(可能有人说可以用Lock加锁,Lock底层的AQS也是基于CAS进行获取锁的)。

public class Test {
    private int i=0;
    public synchronized int add(){
        return i++;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

java中为我们提供了AtomicInteger 原子类(底层基于CAS进行更新数据的),不需要加锁就在多线程并发场景下实现数据的一致性。效率提高了很多

public class Test {
    private  AtomicInteger i = new AtomicInteger(0);
    public int add(){
        return i.addAndGet(1);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3、CAS的问题

乐观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候会判断一下再此期间别人有没有去更新这个数据,可以使用版本号等机制,乐观锁适用于多读的应用类型,这样可以提高吞吐量,乐观锁策略:提交版本必须大于记录当前版本才能执行更新。

悲观锁

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿到这个数据就会block直到它拿到锁。传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁。

CAS 方式为乐观锁,synchronized 为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优。

但使用 CAS 方式也会有几个问题:

3.1 ABA问题

1、 问题说明

比如说一个线程one从内存位置V中取出A,这个时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将 V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

2、解决方案

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。

从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前版本是否等于预期版本,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)

public static void main(String[] args) {

        // AtomicStampedReference 注意:如果泛型是一个包装类,注意对象的引用问题
        AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<Integer>(1,2);  // 当前引用为1,当前版本为2
        new Thread(()->{
            int stamp = atomicReference.getStamp();//获得版本号为2
            System.out.println("A1->"+stamp);   // (1)  A1->2
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // 当前引用等于预期引用,当前版本等于预期版本,满足条件,则将当前引用改为2,当前版本加1等于3
            System.out.println(atomicReference.compareAndSet(1, 2,
                    atomicReference.getStamp(), atomicReference.getStamp() + 1));  // (3) true  
            System.out.println("A2->"+atomicReference.getStamp()); // (3) A2->3
            
           // 当前引用等于预期引用,当前版本等于预期版本,满足条件,则将当前引用改为1,当前版本加1等于4
            System.out.println(atomicReference.compareAndSet(2, 1,
                    atomicReference.getStamp(), atomicReference.getStamp() + 1));  // (4) true  
            System.out.println("A3->"+atomicReference.getStamp()); (4) A3->4
        },"A").start();



        new Thread(()->{
            int stamp = atomicReference.getStamp();//获得版本号为2
            System.out.println("B1->"+stamp);  //(2)  B1->2
            new ReentrantLock(true);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
           // 当前引用(1)等于预期引用(1),但是当前版本(4)不等于预期版本(2)(我们的预期版本应该是B1刚开始拿的,为2),不满足条件,所以当前引用依然是1,当前版本依然是4,成功解决ABA问题
            System.out.println(atomicReference.compareAndSet(1, 6,   // (5) flase  
                    stamp, stamp + 1));
            System.out.println("B2->"+atomicReference.getStamp());  // (5) B2->4
        },"B").start();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

结果:

A1->2
B1->2
true
A2->3
true
A3->4
false  
B2->4
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.2 循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行命令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率

3.3 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。 还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。 从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

三、UnSafe类详解

1、UnSafe的概念、优点和缺点

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法作用),如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用优点)。
但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”缺点),因此对Unsafe的使用一定要慎重

2、UnSafe类的总体功能

在这里插入图片描述
Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,下面将对其相关方法和应用场景进行详细介绍

3、UnSafe与CAS

我们首先看一下AtomicInteger下的CAS命令,AtomicInteger.compareAndSet(期望值,更新值)

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
  • 1
  • 2
  • 3

可以看到它是调用了UnSafe类下的compareAndSwapInt(this, valueOffset, expect, update)方法,继续往下看

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
  • 1

compareAndSwapInt()是一个本地方法

在UnSafe类中存在着这些方法,它们是通过调用compareAndSwapInt()实现的,同时也是CAS的原理所在
getAndAddInt()为例

    public final int getAndAddInt(Object var1, long var2, int var4) {  // var4是偏移量
        int var5; // var5是期望值
        do {
            // 获取传入对象的地址
            var5 = this.getIntVolatile(var1, var2);
            // 比较并交换,如果var1,var2 还是原来的 var5,就执行内存偏移+1; var5 + var4
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

从源码中发现,内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试)。

又从Unsafe类中发现,原子操作其实只支持下面三个方法

public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们发现Unsafe只提供了3种CAS方法:compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong。都是native方法。

四、AtomicInteger

1、 AtomicInteger的常用 API

public final int get():获取当前的值
public final int getAndSet(int newValue):获取当前的值,并设置新的值
public final boolean compareAndSet(int expect, int update):当前值和期望值比较,相同则设置新值
public final int getAndIncrement():获取当前的值,并自增
public final int getAndDecrement():获取当前的值,并自减
public final int getAndAdd(int delta):获取当前的值,并加上预期的值
void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2、 AtomicInteger的优势

传统的Integer需要加synchronized锁,才能保证原子性,但是synchronized锁是重量级锁,效率较低

private volatile int count = 0;
// 若要线程安全执行执行 count++,需要加锁
public synchronized void increment() {
    count++;
}
public int getCount() {
    return count;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

使用 AtomicInteger 后:不需要加锁,也能够保证原子性,实现线程安全

private AtomicInteger count = new AtomicInteger();
public void increment() {
    count.incrementAndGet();
}
// 使用 AtomicInteger 后,不需要加锁,也可以实现线程安全
public int getCount() {
    return count.get();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3、AtomicInteger的源码

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            //用于获取value字段相对当前对象的“起始地址”的偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    //返回当前值
    public final int get() {
        return value;
    }

    //递增加detla
    public final int getAndAdd(int delta) {
        //三个参数,1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(value+delta)。
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    //递增加1
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
...
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

我们可以看到 AtomicInteger 底层用的是volatile的变量和CAS来进行更改数据的。

  • volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值
  • CAS 保证数据更新的原子性。

五、面试问题

1、线程安全的实现方法有哪些?

  • 互斥同步: synchronized 和 ReentrantLock
  • 非阻塞同步: CAS, AtomicXXXX
  • 无同步方案: 栈封闭,Thread Local,可重入代码

2、什么是CAS? CAS使用示例,结合AtomicInteger给出示例?

简单解释就是:
真实值和期望值相同,就修改成功,真实值和期望值不同,就修改失败!
AtomicInteger为例去理解CAS的概念:

AtomicInteger atomicInteger = new AtomicInteger(2020);  // 旧值
boolean ato = atomicInteger.compareAndSet(2020, 2021);  // 分别为期望值和新值,ato为true
System.out.println(atomicInteger.get());  // 结果为:2021
  • 1
  • 2
  • 3

public final boolean compareAndSet(int expect, int update) // 如果我期望值的达到了,那么就更新,否则,就不更新(会一直循环,一直往下看源码可知),CAS是CPU的并发原语

3、CAS会有哪些问题? 针对这这些问题,Java提供了哪几个解决的?

  • ABA问题(版本号去解决)
  • 循环时间开销大(PAUSE指令)
  • 只能保证一个共享变量的原子操作(多个共享变量合并成一个共享变量来操作)

4、AtomicInteger底层实现?

CAS+volatile

5、请阐述你对Unsafe类的理解?

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法作用),如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用优点)。
但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”缺点),因此对Unsafe的使用一定要慎重

6、说说你对Java原子类的理解? 包含13个,4组分类,说说作用和使用场景。

7、AtomicStampedReference是什么?

原子更新引用类型, 内部使用Pair来存储元素值及其版本号,可以解决ABA问题

8、AtomicStampedReference是怎么解决ABA的?

内部使用Pair来存储元素值及其版本号

9、java中还有哪些类可以解决ABA的问题?

AtomicMarkableReference

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/996119
推荐阅读
相关标签
  

闽ICP备14008679号