对JUC的学习和理解(三)atomic 原子类 ,CAS

目录

Atomic 原子类

关于Atomic原子类

为什么要用到原子类呢?

原子类的分类

基本类型原子类

CAS

ABA问题

AtomicInteger 线程安全原理简单分析


Atomic 原子类

java.util.concurrent 包中还有一个包,叫atomic,里面存放着原子类,那么什么是原子类呢?

关于Atomic原子类

从化学上来讲,原子类是构成一般物质上的最小单位,在化学反应中不可分割。在Java中,Atomic是指一个操作是不可中断的,即使是在多个线程执行的时候。一个操作一旦开始,就不会被其他线程干扰,直白的来说,原子类就是具有原子操作特征的类。

为什么要用到原子类呢?

因为在并发编程中,想要保证一些操作不被其他线程干扰,就需要保证原子性。

在《阿里巴巴Java开发手册》中有提到过:

对于一写多读,是可以解决变量同步问题, 但是如果多写,同样无法解决线程安全问题。如果是 count++操作,使用如下类实现: AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。

也就说在实现 i++这样的过程中,如果是多线程下,会造成线程安全问题,在JMM模型中,多线程对于主存的操作都是拷贝到自己的本地的工作内存去操作的,当线程A拿到变量并修改完成放入主存中,线程B还拿着原来变量的变量拷贝。

比如这段代码,运行后,明显有线程安全的问题,顺序混乱,数字重复,也不一定总是能数到1000。

/**
 * @author Claw
 * @date 2020/7/16 0:59.
 */
public class AtomicDemoTest {
    public static void main(String[] args) {
        DataTest data = new DataTest();
        for (int i = 1; i <= 1000; i++) {
            new Thread(() -> {
                // 调用自增方法
                data.increment();
                // 得到count的值
                System.out.println(data.getCount());
            }).start();
        }
    }
}

/**
 * 共享资源
 */
class DataTest {
    private  int count = 0;
    public void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}

遇到这种数据不一致的问题,volatile可以解决,但volatie不能解决i++这样原子性的问题,因为volatile不保证原子性。多个线程同时读取正共享变量的值,就算能保证可见性,也不能保证线程之间读取到同样的值然后互相覆盖对方的值。

当然synchronized可以解决线程安全问题,但你知道,我们这里讲的atomic原子类,所以我们会用atomic原子类解决这个线程安全的问题。

使用synchronized类与atomic类有什么区别呢?原因在于atomic使用的CAS算法,效率更高。至于CAS,会再接下来说到。

原子类的分类

参考: JavaGuide/ docs / java / Multithread / Atomic.md

根据操作的数据类型,可以把JUC包中的原子类分为4类,虽然原子类有很多,但是这里先以AtomicInteger为例进行讲解,以后再研究其他的,我的目的是想理解原子类的基本操作和CAS。

基本类型

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

引用类型

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

数组类型

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

对象的属性修改类型

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

基本类型原子类

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

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

以上三个类提供的API几乎相同,以AtomicInteger为例

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

 基本数据类型原子类的优势

①多线程环境不使用原子类保证线程安全(基本数据类型),也就是加上了synchornized来保证线程安全。

/**
 * 共享资源
 */
class DataTest {
    private  int count = 0;
    public  synchronized void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}

②多线程环境使用原子类保证线程安全(基本数据类型),不再使用synchornized也能保证线程安全。

/**
 * 共享资源
 */
class AtomicData {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.getAndIncrement();
    }

    public AtomicInteger getCount() {
        return count;
    }
}

要对AtomicInteger 底层原理有所了解,需要先了解CAS

CAS

参考:还在用Synchronized?Atomic你了解不?

其实是compare and swap的缩写,直译出来就是比较再交换

CAS是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

CAS有3个操作数:

  • 内存值V
  • 旧的预期值A
  • 要修改的新值B

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值(A和内存值V相同时,将内存值V修改为B),而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试(或者什么都不做)

在简单点的解释,就是比较当前工作内存中的值和主存中的值,如果这个值是期望的,那么执行操作,如果不是期望的值,那么就一直循环。

那么就会有问题存在:

  • 循环会耗时
  • ABA问题(下面会解释)

在Java层面的CAS操作如下:

    public static void main(String[] args) {
        // atomicInteger的值为0
        AtomicInteger atomicInteger = new AtomicInteger(0);
        // 比较再交换 如果期望值为0,那么更新为1 此处返回true 更新成功
        System.out.println(atomicInteger.compareAndSet(0, 1));
        // atomicInteger目前为 1
        System.out.println(atomicInteger);
        // 比较再交换 如果期望值为0,那么更新为1 此处返回false 更新失败 因为atomicInteger目前为1
        System.out.println(atomicInteger.compareAndSet(0, 1));
        System.out.println(atomicInteger);
    }

ABA问题

  • 现在我有一个变量count=10,现在有三个线程,分别为A、B、C
  • 线程A和线程C同时读到count变量,所以线程A和线程C的内存值和预期值都为10
  • 此时线程A使用CAS将count值修改成100
  • 修改完后,就在这时,线程B进来了,读取得到count的值为100(内存值和预期值都是100),将count值修改成10
  • 线程C拿到执行权,发现内存值是10,预期值也是10,将count值修改成1

那么如何解决ABA问题呢?可以使用AtomicStampedReference和AtomicMarkableReference类来解决,但目前留个坑,以后再研究吧。

AtomicInteger 线程安全原理简单分析

相比i++操作,为什么getAndIncrement()能保证原子性?

点击进入AtomicInteger源码,找到incrementAndGet(),可以看到它用到unsafe对象的getAndAddInt()方法

    private static final Unsafe unsafe = Unsafe.getUnsafe();

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

那么Unsafe类又是干什么的?

Java是无法操作内存的,但是Java可以调用C++的native方法,而C++可以操作内存,相当于是Java给自己留的后门,用于操作内存。

来到getAndAddInt()方法,然后看到这段代码,其实最主要的还是compareAndSwapInt(),使用到了CAS的本地方法,也是比较再交换

这段代码,也是自旋锁的体现,do{ }while{ }这段代码块是无限循环的,除非成功,否则一直循环。

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

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

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值