前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >简单理解CAS

简单理解CAS

作者头像
有一只柴犬
发布2024-01-25 11:19:41
960
发布2024-01-25 11:19:41
举报
文章被收录于专栏:JAVA体系JAVA体系

CAS

CAS(Compare And Set)比较交换,是一种无锁算法。即不使用锁的方式来实现多线程同步。由于是无锁的策略,也就是在没有线程被阻塞的情况下实现变量同步,所以也叫非阻塞同步(Non-blocking Synchronization)。

CAS算法:CAS(V, E, N)。

V:要更新的变量。 E:期望值。 N:新值。 当V==E值时,才会将N赋值给V;如果V!=E时,说明已经有别的线程做了更新,当前线程什么都不做(一般是一种自旋的操作,不断的重试-----也是自旋锁)。

基于CAS的线程安全AtomicInteger
代码语言:javascript
复制
package com.thread.atomic;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by shamee-loop on 2020/12/19.
 */
public class AtomicIntegerDemo {
    
    static AtomicInteger i = new AtomicInteger();
    
    public static class AddThread implements Runnable {

        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {
                i.incrementAndGet();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int j = 0; j < 10; j++) {
            threads[j] = new Thread(new AddThread());
        }
        for (int j = 0; j < 10; j++) {
            threads[j].start();
        }
        for (int j = 0; j < 10; j++) {
            threads[j].join();
        }
        System.out.println(i);
    }
}
在这里插入图片描述
在这里插入图片描述

最终程序输出为100000。如果是线程不安全的情况下,输出的值应该是<100000的。 先来看AtomicInteger的incrementAndGet()方法实现:

在这里插入图片描述
在这里插入图片描述

这里的unsafe顾名思义是一个封装了不安全的操作的类。它是sun.misc包下的。这个类是封装了一些类似指针的操作(我们知道C或者C++的指针操作是不安全的,这也是java去除指针的原因,所以暂且这么理解吧)。 我们再跟进去源码:

在这里插入图片描述
在这里插入图片描述

可以看到源码356行,实际上使用了public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);是一个native方法。这里的参数含义: var1:为给定的对象 var2:对象内的偏移量(其实是一个字段到对象头部的偏移量,可以通过这个偏移量来快速定位字段) var3:期望值 var4:要设置的值 看到这里,其实就知道了compareAndSwapInt方法内部必然是使用CAS原子指令来完成的。

CAS优点

1、在高并发下,性能比锁好 2、避免了死锁的情况

CAS缺点
1、CPU开销大

这个很好理解,上面提到在V!=E的情况下,当前线程会通过自旋的方式来不断的重试,直到操作成功。如果长时间不成功,必然会给CPU带来非常大的开销。

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

CAS只对一个共享变量有效,当操作多个共享变量时,CAS无效。JDK1.5开始,添加了AtomicRefrence来保证对象引用之间的原子性。我们可以利用锁或者AtomicRefrence把多个变量放在一个对象里来进行CAS操作。

3、ABA问题

如果变量V的初始值是A,有个线程更新了V的值为B;此时,如果当前线程要读取变量V的时候,又有个线程将V的值改为A,这时候当前线程会误以为V是没有被修改过的(实际上被修改了两次,A->B->A)。这就是ABA问题。

在这里插入图片描述
在这里插入图片描述

举个栗子:

代码语言:javascript
复制
package com.thread.atomic;

import java.util.concurrent.atomic.AtomicReference;

/**
 * 贵宾卡充值案例模拟。
 * 当余额不足20的时候,充值20
 * 另一条线程,当金额大于10的时候,消费10
 * Created by shamee-loop on 2020/12/19.
 */
public class AtomicReferenceDemo {

    static AtomicReference<Integer> money = new AtomicReference<>(15);

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            Integer m = money.get();
            new Thread() {
                public void run() {
                    while (true) {
                        if (m < 20) {
                            if (money.compareAndSet(m, m + 20)) {
                                System.out.println("余额小于20,充值成功。余额=" + money.get());
                                break;
                            }
                        } else {
                            System.out.println("余额大于20,无需充值");
                            break;
                        }
                    }
                }
            }.start();

            new Thread() {
                public void run() {
                        while (true) {
                            Integer m = money.get();
                            if (m > 10) {
                                System.out.println("余额大于10");
                                if (money.compareAndSet(m, m - 10)) {
                                    System.out.println("消费10元,余额=" + money.get());
                                    break;
                                }
                            } else {
                                System.out.println("余额不足");
                                break;
                            }
                        }
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                }
            }.start();
        }
    }
}

程序输出:

在这里插入图片描述
在这里插入图片描述

这里多充值了一次20。原因就是账户余额被反复修改,修改后值等于原来的值,误以为没有被修改过,所以导致CAS无法正确判断当前数据状态。

ABA问题解决
1、版本号机制

对象内部多维护一个版本号,每次操作的同时版本号+1;CAS原子操作时,不只是判断值的状态,也判断版本号是否等于原来的版本号;就算值相等,版本号不等,也判断为被线程修改过。

2、带有时间戳的对象引用 AtomicStampReference
代码语言:javascript
复制
package com.thread.atomic;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 贵宾卡充值案例模拟。
 * 当余额不足20的时候,充值20
 * 另一条线程,当金额大于10的时候,消费10
 * Created by shamee-loop on 2020/12/19.
 */
public class AtomicReferenceDemo {

    static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(15, 0);

    public static void main(String[] args) {
        Integer stamp = money.getStamp();
        for (int i = 0; i < 3; i++) {
            Integer m = money.getReference();
            new Thread() {
                public void run() {
                    while (true) {
                        if (m < 20) {
                            if (money.compareAndSet(m, m + 20, stamp, stamp + 1)) {
                                System.out.println("余额小于20,充值成功。余额=" + money.getReference());
                                break;
                            }
                        } else {
                            System.out.println("余额大于20,无需充值");
                            break;
                        }
                    }
                }
            }.start();

            new Thread() {
                public void run() {
                        while (true) {
                            Integer m = money.getReference();
                            Integer stamp = money.getStamp();
                            if (m > 10) {
                                System.out.println("余额大于10");
                                if (money.compareAndSet(m, m - 10, stamp, stamp + 1)) {
                                    System.out.println("消费10元,余额=" + money.getReference());
                                    break;
                                }
                            } else {
                                System.out.println("余额不足");
                                break;
                            }
                        }
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                }
            }.start();
        }
    }
}

程序输出:

在这里插入图片描述
在这里插入图片描述

类似版本号机制,这里对象内部不仅维护了对象值,还维护了一个时间戳。当对应的值被修改时,同时更新时间戳。当CAS进行比较时,不仅要比较对象值,也要比较时间戳是否满足期望值,两个都满足,才会进行更新操作。

在这里插入图片描述
在这里插入图片描述

这是内部的实现方式。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CAS
    • ABA问题解决
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档