CAS & Atomic原子性 一步到位

简介: ♨️本篇文章记录的为JUC知识中 CAS并发 和 Atomic原子性 相关内容,已经ABA问题的解决方案

♨️本篇文章记录的为JUC知识中CAS & Atomic原子性相关内容,适合在学Java的小白,也适合复习中,面试中的大佬🙉🙉🙉。
♨️如果文章有什么需要改进的地方还请大佬不吝赐教❤️🧡💛

🏡个人主页 : 阿千弟
如果大家对JUC相关知识感兴趣请点击这里👉👉👉JUC专栏学习

在这里插入图片描述

CAS

概述

CAS 的全称为 Compare-And-Swap ,它是一条 CPU并发原语 ,比较 工作内存值 (预期值) 和 主物理内存 的共享值是否相同,相同则执行规定操作,否则继续比较直到主内存和工作内存的值一致为止。这个过程是原子的

CAS 并发原语是现在 Java 语言中就是 sun.misc 包下的 UnSafe 类中的各个方法。调用 UnSafe 类中的 CAS 方法, JVM 会帮我实现 CAS 汇编指令。这是一种完全依赖于硬件功能 ,通过它实现了原子操作。

就像我们在使用使用乐观锁操作数据库时,也会 通过版本号来保证多线程情况下只有一个线程能执行成功。

操作

atomicInteger.getAndIncrement()

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

调用了 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;
}

做个测试

/**
 * @Date 2022/10/28 15:22
 * @Author 阿千弟
 */
public class AtomicIntDemo {
   
   
    public static void main(String[] args) {
   
   
        AtomicInteger atomicInteger = new AtomicInteger();
        int i = atomicInteger.incrementAndGet();
        System.out.println("i = " + i);

        int andIncrement = atomicInteger.getAndIncrement();
        System.out.println("andIncrement = " + andIncrement);
        System.out.println("atomicInteger.compareAndSet(2,3) = " + atomicInteger.compareAndSet(2, 3));
        System.out.println("atomicInteger.compareAndSet(2,3) = " + atomicInteger.compareAndSet(2, 3));
    }
}

运行结果:
在这里插入图片描述

CAS 问题

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

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

ABA 问题

因为 CAS 需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时则会发现它的值没有发生变化,但是实际上却变化了。

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

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

循环时间长开销大

自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令,那么效率会有一定的提升。

pause 指令有两个作用:

  • 第一,它可以延迟流水线执行命令 (de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;
  • 第二,它可以避免在退出循环的时候因内存顺序冲突 (Memory Order Violation) 而引起 CPU 流水线被清空 (CPU Pipeline Flush),从而提高 CPU 的执行效率。

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

当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量 i = 2,j = a,合并一下 ij = 2a,然后用 CAS 来操作 ij。

从 Java 1.5 开始,JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。

在这里插入图片描述

Unsafe

UnSafe 类中所有的方法都是 native修饰的,也就是说 UnSafe 类中的方法都是直接调用操作底层资源执行响应的任务

但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险. 在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再 “安全”,因此对 Unsafe 的使用一定要慎重。

这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,JDK API 文档也没有提供任何关于这个类的方法的解释。总而言之,对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的。
在这里插入图片描述

Unsafe 与 CAS

AtomicInteger 中使用 unsafe。

在这里插入图片描述

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

原子类

基本类型

常用 API

方法 解释

public final int get() |获取当前的值
public final int getAndSet(int newValue) |获取到当前的值,并设置新的值
public final int getAndIncrement() |获取当前的值,并自增
public final int getAndDecrement() |获取到当前的值,并自减
public final int getAndAdd(int delta) |获取到当前的值,并加上预期的值
public final int incrementAndGet( ) |返回的是加 1 后的值
boolean compareAndSet(int expect,int update) |如果输入的数值等于预期值,返回 true

AtomicInteger 解决 i++

CountDownLatch 如何在程序中使用

在没使用 CountDownLatch 时

package tech.chen.juccode.a14;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Date 2022/10/28 17:04
 * @Author 阿千弟
 */
public class CountDownDemo {
   
   
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    public static void main(String[] args) {
   
   
        for (int i = 0; i < 50; i++) {
   
   
            new Thread(()->{
   
   
                for (int j = 0; j < 100; j++) {
   
   
                    atomicInteger.getAndIncrement();
                }
            }).start();
        }
        System.out.println("atomicInteger.get() = " + atomicInteger.get());
    }
}

结果如下

在这里插入图片描述

发现使用 AtomicInteger 居然没有得到理想值 5000,是 AtomicInteger 的问题吗?显然不可能,实际原因是,累加还没结束,主线程就打印出了结果,不信你可以等两秒。但是,实际操作我们不可能设置一个固定的睡眠时间。CountDownLatch 出现的意义也在此。

package tech.chen.juccode.a14;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Date 2022/10/28 17:04
 * @Author 阿千弟
 */
public class CountDownDemo {
   
   
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    private static CountDownLatch countDownLatch = new CountDownLatch(50);
    public static void main(String[] args) {
   
   
        for (int i = 0; i <50; i++) {
   
   
            new Thread(()->{
   
   
                for (int j = 0; j < 100; j++) {
   
   
                    atomicInteger.getAndIncrement();
                }
                countDownLatch.countDown();
            }).start();
        }
        try {
   
   
            countDownLatch.await();
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
        System.out.println("atomicInteger.get() = " + atomicInteger.get());
    }
}

在这里插入图片描述

AtomicBoolean 中断标识

AtomicBoolean 可以作为中断标识停止线程的方式

//线程中断机制的实现方法
public class AtomicBooleanDemo {
   
   
    public static void main(String[] args) {
   
   
        AtomicBoolean atomicBoolean=new AtomicBoolean(false);

        new Thread(()->{
   
   
            System.out.println(Thread.currentThread().getName()+"\t"+"coming.....");
            while(!atomicBoolean.get()){
   
   
                System.out.println("==========");
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"over.....");
        },"A").start();

        new Thread(()->{
   
   
            atomicBoolean.set(true);
        },"B").start();
    }
}

Atomic Long

AtomicLong 的底层是 CAS + 自旋锁 的思想,适用于低并发的全局计算,高并发后性能急剧下降,原因如下:N 个线程 CAS 操作修改线程的值,每次只有一个成功过,其他 N-1 失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子 cpu 就打高了 (AtomicLong 的自旋会成为瓶颈)

在高并发的情况下,我们使用 LoadAdder

数组类型

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

代码演示:

package tech.chen.juccode.a14;

import java.util.concurrent.atomic.AtomicIntegerArray;

/**
 * @Date 2022/10/28 18:30
 * @Author 阿千弟
 */
public class AtomicArray {
   
   
    public static void main(String[] args) {
   
   
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
        for (int i = 0; i < atomicIntegerArray.length(); i++) {
   
   
            int i1 = atomicIntegerArray.get(i);
            System.out.println("i1 = " + i1);
        }

        int i = atomicIntegerArray.incrementAndGet(0);
        System.out.println("i = " + i);
        int andAdd = atomicIntegerArray.addAndGet(1, 4);
        System.out.println("andAdd = " + andAdd);
    }
}

ABA 问题的解决方案

  1. ABA 问题解决方案是使用 AtomicStampedReference 每修改一次都会有一个版本号
  2. AtomicStampedReference 用来解决 AtomicInteger 中的 ABA 问题,该 demo 企图将 integer 的值从 0 一直增长到 1000,但当 integer 的值增长到 128 后,将停止增长。出现该现象有两点原因:
    1. 使用 int 类型而非 Integer 保存当前值
    1. Interger 对 - 128~127 的缓存 [这个范围才有效,不在这个范围 comareAndSet 会一直返回 false
package tech.chen.juccode.a13;

import java.sql.Time;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @Date 2022/8/28 16:33
 * @Author c-z-k
 */
public class ABAResolveDemo {
   
   
    private static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference(100,1);
    public static void main(String[] args) {
   
   
        int startStamp = stampedReference.getStamp();
        Integer startReference = stampedReference.getReference();
        //模拟主线程运行到一半被别的线程插入操作 1
        new Thread(()->{
   
   
            int stamp = stampedReference.getStamp();
            Integer reference = stampedReference.getReference();
            stampedReference.compareAndSet(reference,101,stamp,stamp+1);
        },"t1").start();
        try {
   
   
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        } 
        //模拟主线程运行到一半被别的线程插入操作 2
        new Thread(()->{
   
   
            int stamp = stampedReference.getStamp();
            Integer reference = stampedReference.getReference();
            stampedReference.compareAndSet(reference,100,stamp,stamp+1);
        },"t2").start();
        try {
   
   
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }

        boolean b = stampedReference.compareAndSet(startReference, startReference + 1, startStamp, startStamp + 1);
        System.out.println(b + "\t" + stampedReference.getReference());
    }
}

运行结果: false 100

特殊的 AtomicMarkableReference,它不是维护一个版本号,而是维护一个 boolean 类型的标记,标记值有修改

在这里插入图片描述

如果这篇【文章】有帮助到你💖,希望可以给我点个赞👍,创作不易,如果有对Java后端或者对redis感兴趣的朋友,请多多关注💖💖💖
🏡个人主页:阿千弟
如果大家对JUC相关知识感兴趣请点击这里👉👉👉JUC专栏学习

目录
相关文章
|
6月前
|
算法 程序员 C语言
C/C++原子操作与atomic CAS底层实现原理
假定有两个操作A 和B,如果从执行A 的线程来看,当另一个线程执行B 时,要么将B 全部执行完,要么完全不执行B,那么A 和B 对彼此来说是原子的。
465 1
C/C++原子操作与atomic CAS底层实现原理
|
6月前
|
缓存
15.unsafe类的CAS是怎么保证原子性的?
15.unsafe类的CAS是怎么保证原子性的?
65 0
15.unsafe类的CAS是怎么保证原子性的?
|
6月前
|
应用服务中间件 Linux 调度
锁和原子操作CAS的底层实现
锁和原子操作CAS的底层实现
55 0
|
6月前
多线程并发锁的方案—原子操作
多线程并发锁的方案—原子操作
|
存储 安全 Java
《探索CAS和Atomic原子操作:并发编程的秘密武器》
《探索CAS和Atomic原子操作:并发编程的秘密武器》
89 0
|
存储 编译器 API
锁与原子操作CAS
锁与原子操作CAS
147 0
|
3月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
69 1
|
3月前
|
Java
什么是 CAS(自旋锁)? 它的优缺点? 如何使用CAS实现一把锁?
该博客文章解释了什么是CAS(自旋锁),包括CAS的基本概念、实现原理、优缺点,以及如何使用CAS实现锁的逻辑,并提供了使用CAS实现锁的Java完整代码示例和测试结果。
什么是 CAS(自旋锁)? 它的优缺点? 如何使用CAS实现一把锁?
|
4月前
|
安全 Oracle Java
(四)深入理解Java并发编程之无锁CAS机制、魔法类Unsafe、原子包Atomic
其实在我们上一篇文章阐述Java并发编程中synchronized关键字原理的时候我们曾多次谈到过CAS这个概念,那么它究竟是什么?
|
3月前
|
缓存 Java 编译器