JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

CAS(比较并交换)是实现并发算法的一种技术,通过硬件保证原子性,常用于无锁编程。Unsafe类是CAS的核心,提供直接内存操作和CAS方法。文章详细介绍了CAS的工作原理、Unsafe类的作用,以及AtomicInteger等原子操作类的使用,还探讨了LongAdder的高效实现和源码分析。此外,文章提到了ABA问题及其解决方案,并讨论了原子操作类在不同场景下的应用和性能对比。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、CAS的简介

1.1、什么是CAS

CAS(Compare And Swap)的缩写,中文翻译成比较并交换,实现并发算法时常用到一种技术;他包含了3个操作数 ----- 内存位置,
,预期原值,更新值。执行CAS操作的时候,将内存位置的值与原值进行比较

  • 如果相匹配,那么处理器会自动将该位置的值更新为新值
  • 如果不匹配,处理不做任何操作,多个线程同时执行CAS操作,只有1个会成功

1.2、使用CAS的前后对比

没有CAS的时候, 我们利用sync和voliate保证符合操作的原子性
在这里插入图片描述
用了原子操作类后之后的操作,保证了i++的原子性,没有加入sync重量级别的锁
在这里插入图片描述

1.3、CAS如何做到不加锁的情况,保证数据的一致性

  • CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
  • 它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
  • CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe 提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
  • 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,
  • 所以在多线程情况下性能会比较好。

1.4、什么是Unsafe类

  • Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地方法来访问,Unsafe相当于是一个后门,基于该类可以直接操作特定内存你的数据.Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接去操作内存,因为java中Cas操作的执行依赖于Unsafe方法
  • CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用诒范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

总结:CAS其实是Unsafe提供的一个方法,并且CAS是系统原语,本身就有执行过程不被中断的特性,天生就有保护原子性的特性

1.5、CAS方法参数详解

在这里插入图片描述

/**
var1: 表示要操作的对象
var2:要操作对象属性地址偏移量
var3:表示需要修改数据的期望值
var4:需要修改为的新值
**/
 boolean compareAndSwapObject(Object var1,long var2,Object var3,Object var4)

1.6、CAS的原理

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :

  • 1 AtomicInteger里面的value原始值为,即主内存中AtomiclInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  • 2线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起
  • 3线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  • 4这时线租A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  • 5线程A重新获取valud值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

1.7、 CAS的缺点

  • 问题1 : 因为do While 循环,所以可能循环时间长,开销比较大
    在这里插入图片描述
    解释:因为cas是处于do while循环中的,如果一直没有修改成果,则CPU处于空转状态;开销比较大;

  • 问题2 :引出ABA问题

问题的产生

  • AB两个线程做操作,主内存的值为1,此时他们进行拷贝,他们各自的空间的值都为1
  • A线程把主内存的值1改为2,然后又该1,
  • 此时B过来来修改至根据cas的期望值,他发现1就是他所期望的值,他认为并没有人对主内存进行修改过

上面过程A线程把数据从1->2->1 ,到了B线程读取的时候,进行比较比较他觉得这数据是没有人动过的,这是不符合CAS的原理的.他只管开头和结尾,不关心中心的内容,这是不对的

如何解决ABA问题

AtomicStampedReference[关心改了多少次,参考下一小节,原子类]

2、原子操作类

原子操作类,有很多这里主要把他们分类成如下几类 ,下面的代码示例中,会从中抽出来最经典的进行讲解

2.1、基本类型原子类

  • AtomicInteger (讲解案例)

public class TestMain {
    static AtomicInteger atomicInteger =   new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            },"t1").start();
        }
//          Thread.sleep(2000);
        System.out.println("获取到的值是:"+atomicInteger.get());
    }


    public static void add(){
        atomicInteger.getAndIncrement();
    }
}

虽然使用了atomicInteger,但是输出结果并不是10000;

原因: 还没有等t1 线程计算完成的时候,就已经主线程就已经获取结果了, 此时我们可以使用CountDownLatch ,让主线程等待子线程结束完毕,在运行

public class TestMain {
    static AtomicInteger atomicInteger = new AtomicInteger(0);
    static CountDownLatch countDownLatch = new CountDownLatch(10);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
                countDownLatch.countDown();
            }, "t1").start();
        }
//          Thread.sleep(2000);
        countDownLatch.await();
        System.out.println("获取到的值是:" + atomicInteger.get());
    }


    public static void add() {
        atomicInteger.getAndIncrement();
    }
}
  • AtomicBoolean
  • AtomicBoolean

2.2、数据类型原子类

  • AtomicIntegerArray(讲解案例)
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
        //对下标为0的位置 ,增加100
        atomicIntegerArray.addAndGet(0,100);
        //对下标为1的元素 ,加1
        atomicIntegerArray.getAndIncrement(1);
        System.out.println(atomicIntegerArray.get(0));
        System.out.println(atomicIntegerArray.get(1));

输出结果
100
1
  • AtomicLongArray
  • AtomicReferenceArray

2.3、引用类型原子类

  • AtomicReference
  • AtomicStampedReference[修改过几次,利用版本号的机制],参考ABA问题
  • AtomicMarkableReference[有没有修改过]
public class TestMain {

    static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<>(100, false);

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            boolean marked = atomicMarkableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"获取的标志位为"+marked);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean compareAndSet = atomicMarkableReference.compareAndSet(100, 1000, marked, !marked);
            System.out.println(Thread.currentThread().getName()+"修改为1000,是否成功"+compareAndSet);
        },"t1").start();

        new Thread(()->{
            boolean marked = atomicMarkableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"获取的标志位为"+marked);
            boolean compareAndSet = atomicMarkableReference.compareAndSet(100, 2000, marked, !marked);
            System.out.println(Thread.currentThread().getName()+"修改为2000,是否成功"+compareAndSet);

        },"t2").start();
        Thread.sleep(2000);
        System.out.println("主线程获取的值为"+atomicMarkableReference.getReference());
    }

}

输出结果
t1获取的标志位为false
t2获取的标志位为false
t2修改为2000,是否成功true
t1修改为1000,是否成功false
主线程获取的值为2000

2.4、对象的属性修改原子类

2.4.1、它能帮我们解决什么问题

作用: 以一种线程安全的方式操作非线程安全对象内的某些字段
没有使用前遇到的问题

class Book {
    private Integer id;
    private String name;
    
    public synchronized void add(){
        id++;
    }
}

在之前我们只是想修改1个id值,却直接加了synchronized; synchronized锁的是一个对象? 有没有什么办法只锁Id呢?

2.4.2、使用要求

  • 更新的对象必须使用public volatile修饰

  • 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法NewUpdater()创建一个更新器,并且需要设置想要更新的类和属性

  • AtomicIntegerFieldUpdater

  • AtomicLongFieldUpdater

  • AtomicReferenceFieldUpdater

2.5、原子操作增强类(jdk1.8才有)

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator(提供了自定义的函数操作)
//这个0就是x,这个1就是y
        LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
        //0+1
        longAccumulator.accumulate(1);
        //1+2
        longAccumulator.accumulate(2);
        System.out.println(longAccumulator.get());

输出结果:3
  • LongAdder(只能用来计算加法,且之能从零开始计算)
LongAdder longAdder = new LongAdder();
        longAdder.increment();
        longAdder.increment();
        longAdder.increment();
        longAdder.increment();
        System.out.println(longAdder.sum());

输出结果 4

LongAdder和和AtomicInterget的性能对比效率对比

package com.tvu.interruput;

//需求:50个线程,每个线程100W次,总点赞数出来;


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

class Reader {
    Long number = 0L;

    /**************sync的效率************************/
    public synchronized void syncAddClick() {
        number++;
    }

    public Long getSyncNumber() {
        return number;
    }

    /**************Atomic的效率************************/
    AtomicLong atomicLong = new AtomicLong();

    public void atomicAddClick() {
        atomicLong.getAndAdd(1);
    }

    public Long atomicGetClick() {
        return atomicLong.get();
    }

    /**************LongAdder的效率************************/
    LongAdder longAdder = new LongAdder();

    public void adderClick() {
        longAdder.increment();
    }

    public Long getAddNumber() {
        return longAdder.sum();
    }

    /**************LongAccumulator的效率************************/
    LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);

    public void accMulatorClick() {
        longAccumulator.accumulate(1);
    }

    public long getAccNumber() {
        return longAccumulator.get();
    }

}

public class TestMain {

    public static final int _1W = 10000;
    public static final int threadNumber = 50;
    Reader reader = new Reader();
    static CountDownLatch syncCountDownLatch = new CountDownLatch(threadNumber);


    public static void main(String[] args) throws InterruptedException {
        Reader reader = new Reader();
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < threadNumber; i++) {
            new Thread(() -> {
                for (int j = 0; j < _1W * 1000; j++) {
                    reader.accMulatorClick();
                }
                syncCountDownLatch.countDown();
            }, "t" + i).start();
        }
        syncCountDownLatch.await();
        long endTime = System.currentTimeMillis();


        //sync耗时时间为12395	 输出结果为500000000
        //System.out.println("sync耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.getSyncNumber());

        //atomic耗时时间为7988	 输出结果为500000000
        //System.out.println("atomic耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.atomicGetClick());

        //AddLong耗时时间为599	 输出结果为500000000
        //System.out.println("AddLong耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.getAddNumber());

       // AccMulator耗时时间为642	 输出结果为500000000
        System.out.println("AccMulator耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.getAccNumber());
    }
}



此时我们不禁好奇,为什么LongAdder效率这么快呢?(详情参考LongAdder源码解析)

3、LongAdder效率这么快(源码分析篇)

3.1、几个比较重要的成员变量以及方法

Striped64(他是LongAdder的父类),他主要包含了如下几个比较重要的属性和方法

  • Cell[] cells 数组,为2的幂,方便以后位运算
  • base:类似于AtomicLong中全局的value值。在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
  • cellsBusy:初始化cells或者扩容cells需要获取锁,0:表示无锁状态 1:表示其他线程已经持有了锁
  • collide:表示扩容意向,false一定不会扩容,true可能会扩容。
  • casCellsBusy():通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
  • NCPU:当前计算机CPU数量,Cell数组扩容时会使用到
  • getProbe():获取当前线程的hash值
  • advanceProbe():重置当前线程的hash值

3.2、LongAdder为什么这么快

  • AtomicInteger 慢的原因: 当线程比较多的时候,利用cas,此时空转的线程就会增多,系统cpu就会有负担
  • 利用Cell[]数组分散热点,将value分散到不同的Cell数组中,不同线程会命中到不同的槽位中,各个线程只对自己的槽中那个值进行cas操作,这样热点就分散了,冲突的概率就减少许多了;如果想要获取真正的Long值,只要将各个槽的变量值累加返回即可

3.3、源码解析

在这里插入图片描述

第一次进longAccumulate: 数组的初始化

  1. 如果线程竞争不激烈,则直接在base基础上进行cas操作
    在这里插入图片描述
  2. 如果线程竞争不激烈,则直接在base基础上进行cas操作
  3. 初始化阶段,创建2个cell数组,进行赋值
    在这里插入图片描述

第二次进来,数组的赋值

  1. 确定槽位,进行cas赋值
    在这里插入图片描述
    2.这里是直接对目前的2个槽位进行赋值,没有进longAccumulate

第三次进来,进longAccumulate,需要根据Cell的状态进入不用的if代码块
在这里插入图片描述

进来的前提 : 目前的有了2个cell槽位,依旧竞争激烈
在这里插入图片描述

此时会根据不同的cell状态,进入到不同的代码分支逻辑模块; 我们先看Cell[]数组已经初始化的情况

状态1: 如果Cell[]数组已经初始化

  • 分支1 有槽位,但是还没有值,进行赋值
    在这里插入图片描述
  • 分支2 槽位cas修改失败,重新抢占
    在这里插入图片描述
    分支3: 有槽位,且有值,直接进行修改
    在这里插入图片描述
    分支4:如果槽位大于cpu的数量,则不扩容
    在这里插入图片描述分支5:新建一个cell数组,进行扩容,迁移
    在这里插入图片描述
    状态2:Cell[]数组未初始化[首次新建]
    在这里插入图片描述
    状态3:Cell[]数组正在初始化
    如果多个线程进行casCellsBusy 修改锁状态失败,则会进入到这个分支;cell初始化不了,就把值加给base进行累加
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值