前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CAS自旋锁到底是什么?为什么能实现线程安全?

CAS自旋锁到底是什么?为什么能实现线程安全?

作者头像
鳄鱼儿
发布2024-05-22 08:58:53
650
发布2024-05-22 08:58:53
举报

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

🍀介绍

CAS(Compare and swap),即比较并交换。我们平时所说的自旋锁或乐观锁,其中的核心操作实现就是CAS。

🍀保证原子操作

CAS 适用于保证原子操作不被干扰。原子操作即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。 在多线程环境下,原子操作是保证线程安全的重要手段。 比如说,假设有两个线程在工作,都想对某个值做修改,就拿自增操作来说吧,要对整数 i 进行自增操作,需要做三个步骤:

  1. 从内存读取 i 的当前值
  2. 对 i 值进行加 1 操作
  3. 将 i 值写回内存

假设两个进程都读取了 i 的当前值,当前值为 0。 这时候 A 线程对 i 加 1 了,B 线程也加 1,最后 i 的是 1 ,而不是 2。

这是因为自增操作不是原子操作,分成的这三个步骤可以被干扰。

再如下面这个例子,10个线程,每个线程都执行 10000次 i++ 操作,我们期望的值是 100000,但是很遗憾,结果总是小于 100000 的。

代码语言:javascript
复制
public class Main {

    static int i = 0;

    private synchronized static void add() {
        i++;
    }

    private static class plus implements Runnable {
        @Override
        public void run() {
            for (int k = 0; k < 10000; k++) {
                add();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new plus());
            threads[i].start();
        }

        for (int i = 0; i < 10; i++) {
            threads[i].join();
        }
        System.out.println(i);
    }
}

可以加锁或者利用 synchronized 实现:

代码语言:javascript
复制
public synchronized static void add(){
        i++;
    }

或者,加锁操作,如使用 ReentrantLock (可重入锁)实现。

代码语言:javascript
复制
private static Lock lock = new ReentrantLock();
    public static void add(){
        lock.lock();
        i++;
        lock.unlock();
    }

🍀CAS 实现自旋锁

既然用锁或 synchronized 关键字可以实现原子操作,那么为什么还要用 CAS 呢,因为加锁或使用 synchronized 关键字带来的性能损耗较大,而用 CAS 可以实现乐观锁,它实际上是直接利用了 CPU 层面的指令,所以性能很高。

上面也说了,CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果。自旋就是循环,一般是用无限循环实现。这样一来,一个无限循环中,执行一个 CAS 操作,当操作成功,返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。

其实 JDK 中有好多地方用到了 CAS ,上篇博文中说到ConcurrentHashMap中元素添加是线程安全的,就是利用CAS自旋锁实现的。

代码语言:javascript
复制
	//...其他代码省略
        // CAS 自旋,保证线程安全
        while ((tab = table) == null || tab.length == 0) {
        	// sizeCtl 小于0,表示正在初始化 或 正在扩容
            if ((sc = sizeCtl) < 0)
            	// 让出线程
                Thread.yield(); // lost initialization race; just spin
            // CAS(自旋锁) 修改sizeCtl的值为-1.成功则继续初始化,失败则继续自旋
            // compareAndSwapInt 读取传入当前内存中偏移量为SIZECTL位置的值与期望值sc作比较。相等就把-1赋值给SIZECTL位置的值,再返回true。不相等则返回false。
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                    	// sizeCtl为0,取默认长度DEFAULT_CAPACITY=16,否则用sizeCtl的值(回顾:sizeCtl为正数且未初始化表示数组初始容量)
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        // 构建数组对象
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        // 计算扩容阈值=数组初始容量*0.75(回顾:sizeCtl为正数且已初始化表示数组的扩容阈值)
                        sc = n - (n >>> 2);
                    }
                } finally {
                	// 扩容阈值赋值给sizeCtl
                    sizeCtl = sc;
                }
                break;
            }
        }
      //...其他代码省略

其中用到了 compareAndSwapInt 方法,这就是实现 CAS 的核心方法了。 unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是个 native 方法,是用 c++ 实现的。作用在于利用了 CPU 的 cmpxchg 指令完成比较并替换。这也是CAS(比较并交换)的思想,用于保证并发时的无锁并发的安全性。

🍀使用场景

  • CAS 适合简单对象的操作,比如布尔值、整型值等
  • CAS 适合冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致 CPU 开销很大

🍀ABA问题

CAS 存在一个问题,就是一个值从 A 变为 B ,又从 B 变回了 A,这种情况下,CAS 会认为值没有发生过变化,但实际上是有变化的。对此,并发包下倒是有 AtomicStampedReference 提供了根据版本号判断的实现,可以解决一部分问题。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 🍀介绍
  • 🍀保证原子操作
  • 🍀CAS 实现自旋锁
  • 🍀使用场景
  • 🍀ABA问题
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档