当前位置: 代码迷 >> 综合 >> JUC Atomic:原子类解析
  详细解决方案

JUC Atomic:原子类解析

热度:16   发布时间:2023-12-15 23:32:47.0

文章目录

  • 一、JUC 包中的原子类是哪 4 类?
  • 二、CAS详解
    • 1、CAS的概念
    • 2、CAS的好处
    • 3、CAS的问题
      • 3.1 ABA问题
      • 3.2 循环时间长开销大
      • 3.3 只能保证一个共享变量的原子操作
  • 三、UnSafe类详解
    • 1、UnSafe的概念、优点和缺点
    • 2、UnSafe类的总体功能
    • 3、UnSafe与CAS
  • 四、AtomicInteger
    • 1、 AtomicInteger的常用 API
    • 2、 AtomicInteger的优势
    • 3、AtomicInteger的源码
  • 五、面试问题
    • 1、线程安全的实现方法有哪些?
    • 2、什么是CAS? CAS使用示例,结合AtomicInteger给出示例?
    • 3、CAS会有哪些问题? 针对这这些问题,Java提供了哪几个解决的?
    • 4、AtomicInteger底层实现?
    • 5、请阐述你对Unsafe类的理解?
    • 6、说说你对Java原子类的理解? 包含13个,4组分类,说说作用和使用场景。
    • 7、AtomicStampedReference是什么?
    • 8、AtomicStampedReference是怎么解决ABA的?
    • 9、java中还有哪些类可以解决ABA的问题?

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

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

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

public class Test {
    private  AtomicInteger i = new AtomicInteger(0);public int add(){
    return i.addAndGet(1);}
}

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,当前版本为2new Thread(()->{
    int stamp = atomicReference.getStamp();//获得版本号为2System.out.println("A1->"+stamp);   // (1) A1->2try {
    TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
    e.printStackTrace();}// 当前引用等于预期引用,当前版本等于预期版本,满足条件,则将当前引用改为2,当前版本加1等于3System.out.println(atomicReference.compareAndSet(1, 2,atomicReference.getStamp(), atomicReference.getStamp() + 1));  // (3) true System.out.println("A2->"+atomicReference.getStamp()); // (3) A2->3// 当前引用等于预期引用,当前版本等于预期版本,满足条件,则将当前引用改为1,当前版本加1等于4System.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();//获得版本号为2System.out.println("B1->"+stamp);  //(2) B1->2new 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();}

结果:

A1->2
B1->2
true
A2->3
true
A3->4
false  
B2->4

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

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

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

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

从源码中发现,内部使用自旋的方式进行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);

我们发现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设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

2、 AtomicInteger的优势

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

private volatile int count = 0;
// 若要线程安全执行执行 count++,需要加锁
public synchronized void increment() {
    count++;
}
public int getCount() {
    return count;
}

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

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

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;}//递增加detlapublic final int getAndAdd(int delta) {
    //三个参数,1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(value+delta)。return unsafe.getAndAddInt(this, valueOffset, delta);}//递增加1public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}
...
}

我们可以看到 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

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