深入浅出Java多线程(十):CAS

这篇具有很好参考价值的文章主要介绍了深入浅出Java多线程(十):CAS。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

引言


大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第十篇内容:CAS。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!

在多线程编程中,对共享资源的安全访问和同步控制是至关重要的。传统的锁机制,如synchronized关键字和ReentrantLock等,能够有效防止多个线程同时修改同一数据导致的竞态条件(race condition),但同时也带来了一定的性能开销。尤其是在高并发场景下,频繁的加锁解锁操作可能导致线程上下文切换加剧、系统响应延迟等问题。

为了应对这一挑战,Java从JDK 1.5版本开始引入了基于CAS(Compare And Swap)机制的原子类库,这些原子类不仅提供了一种无锁化的并发控制策略,还能够在不阻塞其他线程的情况下实现高效的内存同步。CAS作为乐观锁的一种实现方式,其核心思想是在更新变量时仅当该变量的当前值与预期值相等时才会执行更新操作,否则就放弃更新并允许线程继续尝试或采取其他策略。

例如,在一个简单的场景中,假设有一个被多个线程共享的整型变量i,若我们想要通过CAS将其从初始值5原子性地递增到6,可以利用AtomicInteger类中的compareAndSet方法:

import java.util.concurrent.atomic.AtomicInteger;

public class CASExample {
    private static AtomicInteger sharedValue = new AtomicInteger(5);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                int oldValue = sharedValue.get();
                if (sharedValue.compareAndSet(oldValue, oldValue + 1)) {
                    System.out.println("Thread " + Thread.currentThread().getName() + " updated the value to " + (oldValue + 1));
                    break;
                }
            }
        });

        t1.start();
        // 确保t1有机会更新值
        t1.join();

        // 输出结果应为:Thread Thread-0 updated the value to 6
    }
}

在这个示例中,如果sharedValue的当前值确实是5,那么线程t1将成功地将它更改为6,并退出循环;如果有其他线程在此期间改变了sharedValue的值,则t1会不断重试直至成功。由于CAS操作直接由CPU指令级别保证其原子性,因此不会出现因并发写入导致的数据混乱。

通过深入探讨Java多线程中的CAS技术,我们将揭示其背后的具体实现原理——Unsafe类及其native方法,剖析AtomicInteger等原子类如何借助CAS机制实现在无锁环境下的高效并发操作,并进一步讨论在实际应用中可能出现的问题,如ABA问题、循环自旋消耗过大以及只能针对单个变量进行原子操作的局限性及其相应的解决方案。

在多线程编程领域中,锁机制是实现数据同步和避免并发问题的关键手段。其中,乐观锁与悲观锁作为两种不同的并发控制策略,在处理共享资源时采用了截然不同的假设和操作方式。

悲观锁&乐观锁


悲观锁

悲观锁,顾名思义,采取保守的策略对待并发访问。它假定每次对共享资源进行操作时都可能发生冲突,因此在执行任何更新前都会预先锁定资源。例如,在Java中使用synchronized关键字或ReentrantLock等工具实现悲观锁时,一个线程在获取锁后才能进入临界区执行代码,其他线程则必须等待锁释放后才能获得执行机会。以下是一个简单的悲观锁示例:

public class PessimisticLockExample {
    private final Lock lock = new ReentrantLock();

    public void decrementCounter() {
        lock.lock(); // 获取悲观锁
        try {
            // 临界区代码
            int count = this.count;
            if (count > 0) {
                this.count--;
            }
        } finally {
            lock.unlock(); // 释放悲观锁
        }
    }

    // 共享资源变量
    private int count = 10;
}

在这个例子中,当一个线程试图修改计数器时,会先锁定整个方法,确保同一时间只有一个线程能够执行减一操作。这种机制虽然保证了数据一致性,但可能造成线程间的频繁阻塞和上下文切换,尤其在高并发环境下性能损耗明显。

乐观锁

相对而言,乐观锁则是基于积极乐观的假设:认为大部分情况下多个线程同时访问同一资源并不会发生冲突。因此,乐观锁允许线程无须获取锁就可以执行业务逻辑,仅在更新数据时采用CAS(Compare And Swap)原子操作检查并更新数据。如果发现数据已被其它线程改变,则放弃本次更新,通常会重新读取数据并再次尝试。

以Java中的AtomicInteger为例,它利用CAS机制实现了乐观锁的特性:

public class OptimisticLockExample {
    private final AtomicInteger counter = new AtomicInteger(10);

    public void incrementCounter() {
        while (true) { // 自旋
            int currentValue = counter.get();
            int newValue = currentValue + 1;
            if (counter.compareAndSet(currentValue, newValue)) { // 使用CAS原子操作
                break// 更新成功,退出循环
            }
        }
    }
}

// AtomicInteger 的 compareAndSet 方法源码简化示意
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

上述代码展示了如何在一个循环内连续尝试原子地增加计数器值。只有当当前值等于预期值时,CAS操作才会成功,否则线程将不断重试直至成功更新。由于乐观锁在没有冲突的情况下不涉及线程挂起,故适用于“读多写少”的场景,能有效降低加锁开销,提高系统吞吐量。然而,若并发更新频率较高,可能会导致大量的CAS失败和重试,从而带来额外的CPU消耗。

CAS原理


在并发编程中,CAS(Compare and Swap,比较并交换)是一种无锁算法,它在不阻塞其他线程的情况下实现原子性的变量更新操作。在Java中,CAS的实现基于Unsafe类提供的native方法,这些方法直接与底层硬件交互,利用CPU级别的原子指令来保证数据更新的安全性。

CAS流程

在CAS操作中涉及三个关键值:V(要更新的变量),E(预期值),N(新值)。当需要对一个共享变量进行修改时,线程首先检查该变量当前值是否等于预期值E。如果相等,则将变量值更新为新值N;如果不等,则说明已经有其他线程更新了该变量,此时当前线程放弃更新操作,保持原值不变。

以AtomicInteger为例,我们可以通过以下代码片段理解CAS的工作过程:

import java.util.concurrent.atomic.AtomicInteger;

public class CASTest {
    private AtomicInteger counter = new AtomicInteger(5);

    public void increment() {
        int expectedValue = counter.get();
        while (!counter.compareAndSet(expectedValue, expectedValue + 1)) {
            // 当前线程获取到的值已经被其他线程改变,重新获取最新值
            expectedValue = counter.get();
        }
    }
}

在这个例子中,compareAndSet方法会不断尝试将计数器从旧值递增1,直到成功为止。当多个线程同时尝试增加计数器时,只有一个线程能够通过CAS成功更新,其余线程将继续循环直至其看到的预期值和实际值匹配后再尝试更新。

原子性和操作系统

CAS的核心优势在于其原子性——即整个比较和交换的操作作为一个不可分割的整体执行。在现代多核CPU架构下,诸如cmpxchg指令这样的原子指令能够确保在没有外部干预的情况下完成这一系列步骤。在Linux X86系统中,cmpxchgl指令配合lock前缀可以确保在同一时刻仅有一个处理器能成功更新内存位置,从而避免了并发问题。

ABA问题

尽管CAS机制在大多数情况下表现优异,但存在一种特殊情况——ABA问题。假设一个变量初始值为A,被更改为B后又改回A,这种情况下使用单纯的CAS检查将会误判为未发生过变化。为了应对ABA问题,JDK提供了一个名为AtomicStampedReference的类,它在每个对象引用上附加了一个版本号或时间戳,使得每次更改不仅检查引用本身,还检查版本号,只有两者都匹配时才会进行替换。

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABATest {
    private AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(10);

    public void update(int newValue, int newStamp) {
        while (true) {
            int currentStamp = ref.getStamp();
            if (ref.compareAndSet(1, newValue, currentStamp, newStamp)) {
                break// 更新成功
            } else {
                // 失败则重试,获取最新的stamp
            }
        }
    }
}

在上述代码中,compareAndSet方法不仅要比较引用对象的值,还要比较并更新相关联的版本信息,因此有效防止了ABA问题的发生。

综上所述,CAS作为一种高效的无锁同步机制,在Java多线程编程中扮演着重要角色,通过直接调用CPU指令实现了并发环境下的原子操作,但也需要注意潜在的ABA问题以及长时间自旋带来的性能开销等问题,并选择合适的解决方案。

Unsafe类


在Java中,为了能够直接与底层硬件进行交互并执行原子操作,如CAS,Java使用了一个名为sun.misc.Unsafe的类。由于该类提供了一些不受JVM访问控制约束的方法,并允许开发者直接操作内存和执行非安全但高效的原语操作,因此被称为“Unsafe”。尽管这个类不在公共API中,但在并发包java.util.concurrent.atomic中的原子类,如AtomicInteger等,都依赖于Unsafe类提供的CAS操作来保证线程间的原子性和可见性。

Unsafe类与CAS方法 Unsafe类包含了一系列native方法,这些方法用于执行原子性的CAS操作,例如:

public native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
public native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
public native boolean compareAndSwapLong(Object o, long offset, long expected, long x);

这些方法分别用于比较并交换对象引用、整型值以及长整型值。参数含义如下:

  • o:一个对象实例,CAS操作将作用在其内部的一个字段上。
  • offset:指定字段相对于对象起始地址的偏移量,由objectFieldOffset()方法计算得出。
  • expected:期望的旧值,只有当字段当前值等于此预期值时,才会进行更新。
  • x:新值,如果条件满足,则用新值替换旧值。

以AtomicInteger为例,其getAndAddInt方法就利用了Unsafe类的compareAndSwapInt方法实现原子递增:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset); // 获取当前值
    } while (!weakCompareAndSetInt(o, offset, v, v + delta)); // 使用CAS尝试更新
    return v; // 返回更新前的值
}

这里首先获取到共享变量的当前值v,然后在一个循环中不断尝试通过CAS指令将变量从v更新为v+delta,直到成功为止。

CPU级别的原子操作 值得注意的是,CAS操作在Java中的实现实际上调用了操作系统和CPU提供的原子指令。在Linux X86系统下,是通过cmpxchgl这样的CPU指令实现的,而在多处理器环境中,为了确保跨多个CPU核心的原子性,还需要配合lock前缀指令锁定总线或缓存行,防止其他处理器同时修改同一数据。

弱版本CAS与强版本CAS的区别 从JDK 9开始,Unsafe类提供了两个看似相似但实际上可能有不同实现策略的方法:compareAndSetIntweakCompareAndSetInt。虽然在早期版本中它们的行为一致,但在某些情况下,weakCompareAndSet系列方法可能只保留了volatile变量本身的特性,而放弃了happens-before规则带来的内存语义保障。这意味着weakCompareAndSet无法确保除了目标volatile变量以外的其他变量的操作顺序和可见性,从而有可能带来更高的性能,但也可能需要开发人员更小心地处理并发逻辑。

总之,Java通过Unsafe类实现了对CAS原子操作的支持,使得程序员可以在高级语言层面上利用底层硬件的原子指令,构建出高效且无锁化的并发程序。然而,这也要求开发者具备对并发编程机制深刻的理解,以便正确解决潜在的问题,比如ABA问题,以及合理应对CAS自旋可能导致的性能开销。

AtomicInteger源码简析


Java并发包中的java.util.concurrent.atomic.AtomicInteger类是一个基于CAS实现的线程安全整数容器,它提供了一系列原子操作方法,如get、set、incrementAndGet等。以getAndAdd(int delta)方法为例,该方法用于获取当前值并原子性地将值增加指定的delta。

Java 17下的Atomic类:

深入浅出Java多线程(十):CAS

首先,我们观察到getAndAdd(int delta)方法调用了Unsafe类的getAndAddInt()方法:

public final int getAndAdd(int delta) {
    return U.getAndAddInt(this, VALUE, delta);
}

这里的UUnsafe类的一个实例,其内部字段VALUE存储了AtomicInteger类中value变量相对于对象起始地址的偏移量。objectFieldOffset()方法用于计算这个偏移量:

private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");

然后,深入到Unsafe类的getAndAddInt()方法实现:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset); // 获取volatile类型的旧值
    } while (!weakCompareAndSetInt(o, offset, v, v + delta)); // 使用CAS更新新值
    return v; // 返回更新前的值
}

这段代码展示了典型的CAS循环模式。首先通过getIntVolatile()读取内存中AtomicInteger实例的volatile变量value的当前值,并保存在局部变量v中。接下来进入一个do-while循环,在循环体内尝试使用weakCompareAndSetInt()执行CAS操作。只有当value的当前值等于我们刚读取到的v时,才会将value设置为v+delta。如果此时value已经被其他线程更改,则CAS失败,程序会再次读取新的value值,并重新进行CAS尝试,直到成功为止。

值得注意的是,这里虽然使用了weakCompareAndSetInt()方法,但在JDK 8及之前版本中,compareAndSetInt()weakCompareAndSetInt()的功能实际上是相同的。而在JDK 9及以上版本中,weakCompareAndSetInt()可能具有更弱的内存语义保证,即不强制满足happens-before规则,这有助于提升性能但要求开发者对并发编程有更深的理解。

通过这种方式,AtomicInteger借助Unsafe提供的底层支持实现了无锁的原子操作,不仅避免了传统锁机制带来的上下文切换开销,还确保了在多线程环境下的数据一致性。同时,通过对源码的分析,我们可以更加深入地理解Java如何利用CAS机制来解决并发问题。

常见问题与解决方案


循环自旋开销问题及其解决方案

使用CAS通常伴随着循环重试机制,即当CAS失败时,线程会不断尝试再次执行CAS操作直至成功。然而,在高竞争条件下,这可能导致线程长时间处于“自旋”状态,占用大量CPU资源且无实质性工作进展。

为了解决这一问题,JVM支持处理器提供的pause指令,比如在HotSpot虚拟机中,可以插入适当的pause指令来降低自旋等待过程中的CPU消耗。pause指令可以使CPU暂时放弃当前线程的执行,并让其他线程有机会运行,从而减少空转带来的性能损失。此外,现代JVM还通过自适应自旋策略调整自旋次数,以达到更好的性能效果。

单变量原子操作局限及其扩展方案

虽然CAS能很好地保证单个共享变量的原子性,但在涉及多个变量的操作场景下,单纯的CAS将显得力不从心。为了应对这种情况,有以下两种解决方案:

  1. 使用AtomicReference类封装对象 当需要对包含多个变量的对象进行原子性更新时,可以利用java.util.concurrent.atomic.AtomicReference类。将多个变量封装到一个对象中,然后对整个对象进行CAS操作,如:

    class Data {
        int a;
        int b;
    }
    AtomicReference<Data> atomicData = new AtomicReference<>(new Data(12));
    // 更新a和b字段的原子操作
    Data newData = new Data(34);
    atomicData.compareAndSet(currentData, newData);

  2. 使用锁保护临界区 在一些复杂的多变量操作场景下,CAS可能无法直接满足需求,此时可以选择传统的锁机制,如synchronized关键字或ReentrantLock类来保护临界区代码,确保在给定时间内只有一个线程能够访问并更新这些变量,从而实现多变量操作的原子性。

综上所述,虽然CAS带来了高效的无锁并发控制机制,但也存在诸如ABA问题、循环自旋开销过大以及只能处理单个变量等问题。针对这些问题,Java平台提供了相应的解决方案,如AtomicStampedReference类、pause指令优化以及AtomicReference等工具,帮助开发者在复杂多样的并发场景下更灵活地运用CAS技术。

总结


在Java多线程编程中,CAS(Compare and Swap)机制扮演着至关重要的角色。作为乐观锁的一种实现方式,它通过比较并交换内存位置的值来保证原子操作,避免了传统悲观锁带来的并发性能瓶颈和上下文切换开销。在JDK的java.util.concurrent.atomic包中,诸如AtomicInteger、AtomicStampedReference等原子类库就是基于Unsafe类提供的CAS原语构建的。

以AtomicInteger为例,其getAndAdd方法利用CAS循环实现了无锁的原子递增操作,确保在高并发场景下变量更新的正确性和高效性。然而,CAS并非完美无缺,其中的ABA问题需要通过引入版本号或时间戳的方式来解决,如AtomicStampedReference通过比较引用与版本戳防止了两次相同值之间的中间状态被忽视。

针对循环自旋导致的CPU资源浪费问题,现代JVM如HotSpot支持处理器pause指令,能够在自旋失败时降低CPU活动频率,减少不必要的消耗。同时,为了克服单个共享变量原子操作的局限性,Java提供了AtomicReference类,可以封装多个变量作为一个整体进行CAS操作,或者在必要时采用锁机制,如synchronized关键字或ReentrantLock,确保多变量间的原子性。

综上所述,CAS为Java开发者提供了一种强大的无锁并发工具,但其使用需结合具体应用场景和可能遇到的问题灵活选择解决方案。只有充分理解并合理应用CAS及其相关技术,才能在实际开发中编写出高性能且线程安全的多线程代码。尽管文档中未给出具体的代码实例,但上述分析和解释已经清晰描绘了如何在Java中运用CAS实现原子操作以及应对相关挑战的过程。文章来源地址https://www.toymoban.com/news/detail-838521.html

到了这里,关于深入浅出Java多线程(十):CAS的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入浅出Java的多线程编程——第二篇

    目录 前情回顾 1. 中断一个线程 1.1 中断的API 1.2 小结 2. 等待一个线程  2.1 等待的API 3. 线程的状态 3.1 贯彻线程的所有状态 3.2 线程状态和状态转移的意义 4. 多线程带来的的风险-线程安全 (重点) 4.1 观察线程不安全 4.2 线程安全的概念 4.3 线程不安全的原因 4.3.1 修改共享数据

    2024年02月07日
    浏览(81)
  • 深入浅出线程池

    线程 (thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际 运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线 程并行执行不同的任务。 既然我们创建了线程,那为何我们直接调用方法和我们调

    2024年02月08日
    浏览(50)
  • 深入浅出C++ ——线程库

      在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了 原子类 的概念。要使用标准库中

    2024年02月03日
    浏览(70)
  • 【深入浅出C#】章节 9: C#高级主题:多线程编程和并发处理

    多线程编程和并发处理的重要性和背景 在计算机科学领域,多线程编程和并发处理是一种关键技术,旨在充分利用现代计算机系统中的多核处理器和多任务能力。随着计算机硬件的发展,单一的中央处理单元(CPU)已经不再是主流,取而代之的是多核处理器,这使得同时执行

    2024年02月11日
    浏览(58)
  • 【昕宝爸爸小模块】深入浅出之JDK21 中的虚拟线程到底是怎么回事(一)

    ➡️博客首页       https://blog.csdn.net/Java_Yangxiaoyuan        欢迎优秀的你👍点赞、🗂️收藏、加❤️关注哦。        本文章CSDN首发,欢迎转载,要注明出处哦!        先感谢优秀的你能认真的看完本文,有问题欢迎评论区交流,都会认真回复! 虚拟线程这个

    2024年01月16日
    浏览(49)
  • 【昕宝爸爸小模块】深入浅出之JDK21 中的虚拟线程到底是怎么回事(二)

    ➡️博客首页       https://blog.csdn.net/Java_Yangxiaoyuan        欢迎优秀的你👍点赞、🗂️收藏、加❤️关注哦。        本文章CSDN首发,欢迎转载,要注明出处哦!        先感谢优秀的你能认真的看完本文,有问题欢迎评论区交流,都会认真回复! 上一篇博文:

    2024年01月16日
    浏览(49)
  • 深入浅出Java中参数传递的原理

    今天,想和大家聊聊关于java中的参数传递的原理,参数的传递有两种,值传递和引用传递。 值传递 :是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 引用传递 :是指在调用函数时将实际参数的地址传递到

    2024年02月01日
    浏览(67)
  • 深入浅出 Java 中的神锁:ReentrantLock,还有谁不会?

    来源:jiannan.blog.csdn.net/article/details/121331360 话不多说,扶我起来,我还可以继续撸。 在学习 ReentrantLock 源码之前,先来回顾一下链表、队列数据结构的基本概念~~ 小学一、二年级的时候,学校组织户外活动,老师们一般都要求同学之间小手牵着小手。这个场景就很类似一

    2024年02月08日
    浏览(46)
  • 大聪明教你学Java | 深入浅出聊 ConcurrentHashMap

    🍊作者简介: 不肯过江东丶,一个来自二线城市的程序员,致力于用“猥琐”办法解决繁琐问题,让复杂的问题变得通俗易懂。 🍊支持作者: 点赞👍、关注💖、留言💌~ 在 Java 的集合框架中,HashMap 是一种非常常用的数据结构,它提供了键值对形式的存储和访问方式。然

    2024年02月10日
    浏览(39)
  • 深入浅出Docker:Java开发者的快速上手指南

    在今天的软件开发环境中,Docker已经成为了一种常见的开发和部署工具。无论你是前端开发者、后端开发者,还是DevOps工程师,理解并掌握Docker都将成为你所必须的技能。对于Java开发者来说,使用Docker可以极大地提高你的生产力。那么,如何使用Docker来部署Java应用呢?本文

    2024年02月07日
    浏览(58)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包