小白都能看懂的CAS基本原理与实战应用指南

简介: 小白都能看懂的CAS基本原理与实战应用指南

引言

最近有个读者问我什么是CAS,今天了不起来聊聊CAS(Compare And Swap)这个概念。

它在日常编程中非常有用,特别是在多线程编程中。

本篇文章将从原理介绍、源码分析、实战应用等方面进行阐述。

一、CAS原理介绍

CAS(Compare And Swap)是一种用于实现无锁并发算法的技术。

大多数时候它也还有其他叫法:无锁优化、自旋、乐观锁

它的核心思想是:通过比较当前需要修改的值与预期原来的值,如果相等,则使用新值进行替换。

这个过程是原子性的,它底层是靠C语言依赖的操作系统的原子操作来保证原子性的,即在这个过程中不会被其他线程打断。

在Java中,CAS操作主要是通过 java.util.concurrent.atomic包中的原子类来实现的,如 AtomicIntegerAtomicLong等。

原理示例

假设有一个整数变量 count,初始值为0。现在有两个线程A和B同时对 count进行加1操作。使用CAS操作可以确保 count最终的值为2。

  1. 线程A读取 count的值,得到0。
  2. 线程B读取 count的值,得到0。
  3. 线程A将 count的值与预期值0进行比较,相等,则将 count的值更新为1。
  4. 线程B将 count的值与预期值0进行比较,不相等(因为已经被线程A更新为1),则重试。
  5. 线程B重新读取 count的值,得到1。
  6. 线程B将 count的值与预期值1进行比较,相等,则将 count的值更新为2。

最终,count的值为2,实现了无锁并发操作。

二、源码分析

java.util.concurrent.atomic.AtomicInteger为例,这个类提供了一个原子的整数值,可以用于实现无锁的整数操作。我们来看一下它的 compareAndSet方法的源码:

public final boolean compareAndSet(int expect, int update){
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

这里的 unsafe是一个 sun.misc.Unsafe类型的对象,它提供了底层的CAS操作。valueOffset是一个静态常量,表示 AtomicInteger内部的 value字段的内存偏移量。expect是预期值,update是新值。这个方法会返回一个布尔值,表示操作是否成功。

小结:

atomicXXX类

  • Compare and Set/Swap 比较并且设定
  • cas(V,Expected,NewValue)
    if V == EV == New otherwise try again or fail
  • CPU原语支持 atomic底层调用到UnSafe这个方法,三个参数V就是当前版本值,Expected期望值,NewValue赋予的新值。
    只有当V等于E的时候才将新的值赋给这个变量,又因为它是原语支持是CPU级别的,是一个原子操作,所以在设值时不会有其他线程来插队设值。

三、实战应用

我们来看一个实际的例子,如何使用 AtomicInteger实现一个无锁的计数器。

public class Counter {
    /*volatile*/ //int count1 = 0;
    AtomicInteger count = new AtomicInteger(0);
    /*synchronized*/ void m() {
        for (int i = 0; i < 10000; i++)
            //if count1.get() < 1000
            count.incrementAndGet(); //count1++
    }
    public static void main(String[] args) {
        Counter t = new Counter();
        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }
        threads.forEach((o) -> o.start());
        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}

在这个例子中,我们创建了一个 Counter类,内部使用 AtomicInteger实现了无锁的计数,然后我们创建了10个线程,分别对计数器进行增加操作。最后我们输出计数器的值,可以看到它确实是10000,证明我们的无锁计数器是正确的。

四、CAS在日常中的应用场景

在实际开发中,我们可能会遇到以下几种使用CAS的场景:

  1. 无锁计数器 :如上面的例子所示,我们可以使用CAS实现一个高效的无锁计数器,避免了使用同步锁带来的性能开销。
  2. 单例模式 :我们可以使用CAS实现一种线程安全的单例模式,确保在多线程环境下只创建一个实例。
  3. 并发容器 :在实现高性能的并发容器时,如 ConcurrentHashMap,我们可以使用CAS操作来实现无锁或低锁的数据结构更新。
  4. 多线程并发任务 :在多线程并发执行任务时,我们可以使用CAS操作来确保任务状态的正确更新,例如实现一个无锁的任务分发器。

五、ABA问题

如果另一个线程修改V值,假设原来是A,先修改成B,再修改回成A。

当前线程的CAS操作无法分辨当前V值是否发生过变化,这个就是ABA问题。

解决:

  • 在CAS的时候加版本号,每次操作比较下版本号
  • 加 version
  • A 1.0
  • B 2.0
  • A 3.0
  • cas(version)
  • 原子类 AtomicStampedReference解决ABA问题
  • ABA问题重要不?如果是基本数据类型结果没影响,如果是引用对象就不好说了,比如你的女朋友和你复合,前面经过了多少个其他XXX,你觉得有影响不?

六、总结

了不起带着大家从原理介绍、源码分析、实战应用等方面讲解了CAS的相关知识。

通过本文的学习,相信你们已经对CAS有了一定的了解,掌握了如何在实际开发中应用CAS来解决并发问题。

当然,CAS并不是万能的,它也有一定的局限性,例如ABA问题。

在实际开发中,我们需要根据具体场景选择合适的并发策略。

目录
打赏
0
1
1
0
56
分享
相关文章
谈谈Java反射:从入门到实践,再到原理
谈谈Java反射:从入门到实践,再到原理
155 0
C++初阶之一篇文章教会你list(模拟实现)(上)
这个表中列出了C++标准库中list容器的一些成员类型定义。这些类型定义是为了使list能够与C++标准库的其他组件协同工作,并提供一些通用的标准接口。每个成员类型
PixiJS源码分析系列: 第一章 从最简单的例子入手
PixiJS源码分析系列: 第一章 从最简单的例子入手
|
10月前
|
前端知识笔记(四)———深浅拷贝的区别,如何实现?
前端知识笔记(四)———深浅拷贝的区别,如何实现?
76 0
C++初阶之一篇文章教会你list(模拟实现)(下)
4.swap void swap(list<T>& x) { std::swap(_head, x._head); // 交换两个链表的头结点指针 } 这是 list 类的成员函数 swap,它用于交换两个链表的内容。
C++初阶之一篇文章教会你list(理解和使用)(中)
3. max_size() max_size() 是 std::list 容器的一个成员函数,用于返回容器可能容纳的最大元素数量,通常受到系统内存限制的影响。它返回一个无符号整数类型,表示容器的最大大小。函数签名如下:
K8S对小白来说有什么用?如何才能学好K8S?底层原理是什么?
K8S对小白来说有什么用?如何才能学好K8S?底层原理是什么?
225 0
初阶 数据结构与算法——经典 八大排序算法||初步学习至熟练掌握(附动图演示,初学者也能看懂)
重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
323 0
面试官:如何实现一个乐观锁(小白都能看得懂的代码)
java多线程中的锁分类多种多样,其中有一种主要的分类方式就是乐观和悲观进行划分的。这篇文章主要介绍如何自己手写一个乐观锁代码。不过文章为了保证完整性,会从基础开始介绍。
625 0
面试官:如何实现一个乐观锁(小白都能看得懂的代码)
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等