【多线程与高并发 四】CAS、Unsafe 及 JUC 原子类详解

👏作者简介:大家好,我是若明天不见,BAT的Java高级开发工程师,CSDN博客专家,后端领域优质创作者

📕系列专栏:多线程及高并发系列

📕其他专栏:微服务框架系列MySQL系列Redis系列Leetcode算法系列GraphQL系列

📜如果感觉博主的文章还不错的话,请👍点赞收藏关注👍支持一下博主哦❤️

时间是条环形跑道,万物终将归零,亦得以圆全完美

CAS、Unsafe 及 JUC 原子类详解


多线程及高并发系列


【多线程及高并发 一】内存模型及理论基础中说到多线程的原子性,i++的操作其实需要三条 CPU 指令:

  1. 将变量 i 从内存读取到 CPU寄存器
  2. 在CPU寄存器中执行 i + 1 操作
  3. 将最后的结果 i 写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)

为了解决原子性问题,保证线程安全,可以使用synchronizedReentrantLock来进行线程互斥同步,也可以使用基于CAS实现的java.util.concurrent.atomic包中原子类进行无锁的非阻塞同步

ReentrantLockAQS的一种实现,其底层操作其实也是使用CAS+volatile

CAS

在Java中,CASCompare and Swap)是一种并发编程技术,用于实现多线程环境下的无锁同步,由于不断循环重试也被称为自旋锁CAS操作包括三个操作数:内存位置(通常是一个变量)、预期原值和新值

CAS操作会比较内存位置的当前值与预期原值是否相等,如果相等,则将该内存位置的值更新为新值;如果不相等,则不做任何操作

CAS操作是一种乐观锁技术,它可以避免使用传统锁带来的性能开销和线程阻塞。然而,需要注意的是,CAS操作并不适用于所有并发场景,特别是在存在大量线程竞争的情况下,CAS操作可能会导致自旋等待,降低性能

CAS操作的简单示例

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class CASExample {
    private static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) {
        // 启动多个线程进行自增操作
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                int oldValue, newValue;
                do {
                    // 获取当前值
                    oldValue = counter.get();
                    // 计算新值
                    newValue = oldValue + 1;
                    // 使用CAS操作尝试更新值
                } while (!counter.compareAndSet(oldValue, newValue));
            }).start();
        }

        // 等待所有线程执行完毕
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出最终结果
        System.out.println("Counter: " + counter.get());
    }
}

相对于互斥同步的synchronizedCAS操作是一种乐观锁技术,它可以避免使用传统锁带来的性能开销和线程阻塞。然而使用CAS操作可能会导致自旋等待、ABA问题、无法保证多变量原子性等问题

自旋等待

当多个线程同时尝试执行CAS操作时,如果某个线程的CAS操作失败,它会一直尝试执行CAS直到成功为止,这个过程称为自旋等待。自旋等待会消耗CPU资源,降低系统的整体性能

无法保证多变量原子性

虽然CAS操作是原子的,但它只能保证单个变量的原子性操作。如果需要对多个变量进行原子操作,就需要使用其他同步机制如锁

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

ABA 问题

ABA问题是指在CAS(Compare and Swap)操作中可能出现的一种情况,其中一个变量的值在开始时是A,然后被改变为B,最后又被改回A。这样的变化序列可能导致CAS操作错误地认为期间没有发生变化,但实际上变化了

为了解决ABA问题,可以使用版本号或标记来追踪变化过程。每次对共享变量进行修改时,都会增加一个版本号或标记,以便在CAS操作中比较变化过程是否符合预期。使得A->B->A 变为 1A->2B->3A

对于常量类型来说,ABA问题可能不一定会影响预期,但是对于对象来说期间被修改过其实不符合预期

AtomicStampedReference类是 Java 中提供的一个用于解决ABA问题的原子类。它通过引入版本号(stamp)来追踪共享变量的变化过程,从而在CAS操作中比较变化过程是否符合预期

Unsafe

Unsafe是Java中的一个特殊类,它提供了直接操作内存和执行低级别操作的功能。虽然它是Java的内部API,不建议在普通应用程序中直接使用它,但它在Java的核心库和一些高级框架中被广泛使用

官方建议开发人员避免直接使用Unsafe类,而是使用 Java 提供的更高级别的并发和内存管理工具,如java.util.concurrent包和java.nio包中的类

Unsafe类的主要功能和特点:

  • 直接内存操作:Unsafe允许直接操作堆外内存,即绕过Java虚拟机的内存管理机制,直接操作内存的原始字节。这种能力对于优化和处理大量数据的高性能应用程序非常有用。
  • 数组操作:Unsafe提供了一些方法来操作数组,例如在数组中获取和设置元素的值,以及进行数组的复制和填充等操作。
  • 对象操作:Unsafe允许直接操作对象的字段,包括获取和设置字段的值,以及对字段进行原子更新等操作。它可以绕过Java语言中的访问权限控制,对私有字段进行访问和修改。
  • 内存屏障和原子操作:Unsafe提供了内存屏障(Memory Barriers)和原子操作的支持,用于在多线程环境下实现并发控制和同步。
  • 类加载和实例化:Unsafe提供了一些方法来加载和实例化类,包括分配类的实例和操作类的静态字段

Unsafe 之 CAS

如下源代码释义所示,这部分主要为CAS相关操作的方法。

java 复制代码
/**
    *  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
  
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

CAS是一条CPU的原子指令(cmpxchg指令 ),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法底层实现即为 CPU 指令cmpxchg

典型应用

CASjava.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等实现上有非常广泛的应用

如下图所示,AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过UnsafeobjectFieldOffset方法获取。

AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址,从而可以根据CAS实现对value字段的原子操作

下图为某个AtomicInteger对象自增操作前后的内存示意图,对象的基地址baseAddress="0x110000",通过baseAddress + valueOffset得到value的内存地址valueAddress="0x11000c";然后通过CAS进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止

内存操作

这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法

java 复制代码
//分配内存, 相当于C++的malloc函数
public native long allocateMemory(long bytes);
//扩充内存
public native long reallocateMemory(long address, long bytes);
//释放内存
public native void freeMemory(long address);
//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
public native byte getByte(long address);
//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);

通常,我们在Java中创建的对象都处于堆内内存中,堆内内存是由 JVM 所管控的 Java 进程内存,并且它们遵循 JVM 的内存管理机制,JVM 会采用垃圾回收机制统一管理堆内存

与之相对的是堆外内存,存在于 JVM 管控之外的内存区域,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法

典型应用

DirectByteBuffer是Java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty 、MINA 等 NIO 框架中应用广泛。

  • Direct Buffer 可以通过ByteBuffer.allocateDirect()方法来创建,它的数据存储在堆外内存中,生命周期内内存地址都不会再发生更改,进而内核可以安全地对其进行访问,很多 IO 操作会很高效
  • 减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高

Direct Buffer 创建和销毁过程中,都会比一般的堆内 Buffer 增加部分开销,所以通常都建议用于长期使用、数据较大的场景

DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存 API 来实现

上图为DirectByteBuffer构造函数,创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放

线程调度

包括线程挂起、恢复、锁机制等方法

java 复制代码
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);

如上源码说明中,方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark可以终止一个挂起的线程,使其恢复正常

Java 锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现

更多Unsafe相关功能,详见Java魔法类:Unsafe应用解析|美团技术团队

Atomic 原子类

JDK 提供了基于CAS实现的一系列原子类,在java.util.concurrent.atomic包下

根据操作的数据类型,可以将 JUC 包中的原子类分为 4 类

基本类型

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

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

数组类型

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

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

引用类型

  • AtomicReference:引用类型原子类
  • AtomicMarkableReference:原子更新带有标记的引用类型。该类提供了一个布尔标记(mark)来表示共享变量的变化,而无法追踪具体的变化过程
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题

对象的属性修改类型

原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类

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

基本类型

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

以 AtomicInteger举例,AtomicInteger 类常用方法

java 复制代码
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 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
java 复制代码
public class AtomicIntegerExample {
    public static void main(String[] args) {
        AtomicInteger counter = new AtomicInteger(0);

        // 增加计数器的值
        counter.incrementAndGet();
        System.out.println("增加后的值: " + counter.get());

        // 减少计数器的值
        counter.decrementAndGet();
        System.out.println("减少后的值: " + counter.get());

        // 使用addAndGet方法增加指定的值
        counter.addAndGet(5);
        System.out.println("增加后的值: " + counter.get());

        // 使用getAndAdd方法获取当前值并增加指定的值
        int previousValue = counter.getAndAdd(10);
        System.out.println("之前的值: " + previousValue);
        System.out.println("增加后的值: " + counter.get());
    }
}

数组类型

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

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

以 AtomicIntegerArray 举例,AtomicIntegerArray 类常用方法

java 复制代码
public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicIntegerArray 类使用示例:

java 复制代码
public class AtomicIntegerArrayExample {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        AtomicIntegerArray atomicArray = new AtomicIntegerArray(array);

        // 原子地增加指定索引位置的元素值
        atomicArray.getAndIncrement(2); // 索引2的元素值增加1
        System.out.println("增加后的值: " + atomicArray.get(2));

        // 原子地减少指定索引位置的元素值
        atomicArray.getAndDecrement(4); // 索引4的元素值减少1
        System.out.println("减少后的值: " + atomicArray.get(4));

        // 原子地增加指定索引位置的元素值,并返回增加前的值
        int previousValue = atomicArray.getAndAdd(1, 10); // 索引1的元素值增加10
        System.out.println("之前的值: " + previousValue);
        System.out.println("增加后的值: " + atomicArray.get(1));
    }
}

引用类型

AtomicStampedReference类是Java中提供的一个用于解决ABA问题的原子类。它通过引入版本号(stamp)来追踪共享变量的变化过程,从而在CAS操作中比较变化过程是否符合预期

AtomicStampedReference类的构造方法如下:

java 复制代码
/**
 * @param initialRef 初始引用
 * @param initialStamp 初始版本号
 */
public AtomicStampedReference(V initialRef, int initialStamp)

AtomicStampedReference类提供了以下主要方法:

  • boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp):尝试以原子方式将引用和版本号设置为新值,只有当当前引用和版本号与期望值相等时才会成功。
  • V getReference():获取当前的引用值。
  • int getStamp():获取当前的版本号。
  • V get(int[] stampHolder):获取当前的引用值,并将当前的版本号存储在stampHolder数组中的第一个元素中。
  • void set(V newReference, int newStamp):设置新的引用值和版本号。

使用 AtomicStampedReference 解决ABA问题的示例:

java 复制代码
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceExample {
    private static AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        // 线程A先将值从A改为B,再改回A
        Thread threadA = new Thread(() -> {
            int stamp = reference.getStamp();
            String value = reference.getReference();

            reference.compareAndSet(value, "B", stamp, stamp + 1);
            reference.compareAndSet("B", "A", stamp + 1, stamp + 2);
        });

        // 线程B在A操作的过程中执行CAS操作
        Thread threadB = new Thread(() -> {
            int stamp = reference.getStamp();
            String value = reference.getReference();

            // 等待线程A完成第一次操作
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean result = reference.compareAndSet(value, "C", stamp, stamp + 1);
            System.out.println("CAS result: " + result);
        });

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();

        System.out.println("Final value: " + reference.getReference());
    }
}

在上述示例中,线程A先将共享变量的值从A改为B,再改回A。线程B在线程A的操作过程中执行CAS操作,尝试将共享变量的值从A改为C。由于AtomicStampedReference引入了版本号,CAS操作会比较当前的引用值和版本号是否与期望值相等,从而避免了ABA问题

对象的属性修改类型

原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类

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

AtomicIntegerFieldUpdater类提供了一种线程安全的方式来对指定类的字段进行原子操作 ,无需使用synchronized关键字或volatile修饰符

注意:AtomicIntegerFieldUpdater仅适用于实例变量,不能用于静态变量

AtomicIntegerFieldUpdater 类使用示例:

java 复制代码
public class AtomicIntegerFieldUpdaterExample {
    public static void main(String[] args) {
        // 定义一个包含一个int类型字段的类
        class MyClass {
            public volatile int myField;
        }

        // 创建AtomicIntegerFieldUpdater对象
        AtomicIntegerFieldUpdater<MyClass> updater = AtomicIntegerFieldUpdater.newUpdater(MyClass.class, "myField");

        // 创建一个MyClass对象
        MyClass obj = new MyClass();

        // 原子地将字段值增加1,并返回增加前的值
        int previousValue = updater.getAndIncrement(obj);
        System.out.println("之前的值: " + previousValue);
        System.out.println("增加后的值: " + updater.get(obj));

        // 原子地将字段值减少1,并返回减少前的值
        previousValue = updater.getAndDecrement(obj);
        System.out.println("之前的值: " + previousValue);
        System.out.println("减少后的值: " + updater.get(obj));

        // 原子地增加字段值,并返回增加前的值
        previousValue = updater.getAndAdd(obj, 10);
        System.out.println("之前的值: " + previousValue);
        System.out.println("增加后的值: " + updater.get(obj));
    }
}

参考资料:

  1. Java魔法类:Unsafe应用解析
  2. `JUC原子类: CAS, Unsafe和原子类详解
  3. 非阻塞同步算法与CAS无锁算法
相关推荐
圈圈编码9 分钟前
Spring Task 定时任务
java·前端·spring
俏布斯21 分钟前
算法日常记录
java·算法·leetcode
276695829226 分钟前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿
爱的叹息27 分钟前
Java 连接 Redis 的驱动(Jedis、Lettuce、Redisson、Spring Data Redis)分类及对比
java·redis·spring
程序猿chen37 分钟前
《JVM考古现场(十五):熵火燎原——从量子递归到热寂晶壁的代码涅槃》
java·jvm·git·后端·java-ee·区块链·量子计算
松韬1 小时前
Spring + Redisson:从 0 到 1 搭建高可用分布式缓存系统
java·redis·分布式·spring·缓存
绝顶少年1 小时前
Spring Boot 注解:深度解析与应用场景
java·spring boot·后端
心灵宝贝1 小时前
Tomcat 部署 Jenkins.war 详细教程(含常见问题解决)
java·tomcat·jenkins
天上掉下来个程小白1 小时前
Redis-14.在Java中操作Redis-Spring Data Redis使用方式-操作列表类型的数据
java·redis·spring·springboot·苍穹外卖
ゞ 正在缓冲99%…2 小时前
leetcode22.括号生成
java·算法·leetcode·回溯