Java中CAS详解

news/2024/5/17 17:47:19 标签: java, cas, ABA, Synchronized, 自旋锁

1、概述

说到CAS就会想到Java中的原子类,也即是java.util.concurrent.atomic包下的类。

咱们先看看在多线程环境下对比使用原子类和不使用原子类怎么保证i++线程安全,以及性能结果。

实例代码:

500个线程,每个线程执行100万次i++

java">package com.lc.test03;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author liuchao
 * @date 2023/4/15
 */
public class CasTest {

    volatile Long value = 0L;

    AtomicLong atomicLong = new AtomicLong(0L);

    public Long getValue() {
        return value;
    }

    public synchronized void addValueBySynchronized() {
        value++;
    }

    public synchronized void addValueByAtomic() {
        atomicLong.incrementAndGet();
    }

    public static void main(String[] args) throws Exception {

        /**
         * 每个线程请求100万次
         */
        int reqNum = 1000000;
        /**
         * 500个线程
         */
        int threadNum = 500;

        CasTest casTest1 = new CasTest();
        CountDownLatch countDownLatch1 = new CountDownLatch(threadNum);
        Long start = System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < reqNum; j++) {
                        casTest1.addValueBySynchronized();
                    }
                } finally {
                    countDownLatch1.countDown();
                }
            }).start();
        }
        countDownLatch1.await();
        Long end = System.currentTimeMillis();
        System.out.println("Synchronized用时:" + (end - start) + "毫秒,最终值:" + casTest1.getValue());


        CasTest casTest2 = new CasTest();
        CountDownLatch countDownLatch2 = new CountDownLatch(threadNum);

        start = System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < reqNum; j++) {
                        casTest2.addValueByAtomic();
                    }
                } finally {
                    countDownLatch2.countDown();
                }
            }).start();
        }
        countDownLatch2.await();
        end = System.currentTimeMillis();
        System.out.println("Atomic用时:" + (end - start) + "毫秒,最终值:" + casTest2.atomicLong.get());

    }
}

Synchronized用时:31110毫秒,最终值:500000000
Atomic用时:27767毫秒,最终值:500000000

可以看出Atomic 效率高一些

正是因为Synchronized这种重量级锁的性能较低,才会有CAS的诞生,那什么是CAS呢?

2、什么是CAS

cas(Compare and swap),中文翻译为比较并交换,这是实现并发算法时常用到的一种技术。

它包含三个操作数:内存位置、预期原值和要更新的值。

执行CAS操作的时候,将内存位置的值与预期原值比较:

如果相匹配,那么处理器会自动将该位置的值更新为新值,

如果不匹配,处理器不做任何处理,多个线程同时执行CAS操作,只会有一个成功。

CAS只是一种算法思想,在Java中落地为原子类,也即是java.util.concurrent.atomic包中的类

 简单使用:

java">        //1、初始化AtomicInteger 并赋初始值为5
        AtomicInteger atomicInteger = new AtomicInteger(3);
        //2、第一次比较并交换   期望值是3,如果和内存位置中的值是一致的,则改为6
        System.out.println(String.format("第一次比较并交换结果:%s,执行后atomicInteger值为:%s",
                atomicInteger.compareAndSet(3, 6), atomicInteger.get()));
        //3、第二次比较并交换   期望值还是3,因为内存值不为3,所以交换返回false
        System.out.println(String.format("第一次比较并交换结果:%s,执行后atomicInteger值为:%s",
                atomicInteger.compareAndSet(3, 6), atomicInteger.get()));

第一次比较并交换结果:true,执行后atomicInteger值为:6
第一次比较并交换结果:false,执行后atomicInteger值为:6

源码解释,其他重载方法原理是一致的。

 

3、为什么CAS性能会比Synchronized

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,通过跟踪代码会发现CAS底层使用的都是Unsafe类,Unsafe提供的CAS方法底层实现即为CPU指令cmpxchg。

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用Synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会相对较好。

4、自旋锁

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果。

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用自旋方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少了由于线程切换带来的消耗,缺点也是非常明显,不断循环会消耗CPU。在AtomicInteger中有这样的方法如下:即为通过CAS+循环实现自旋锁

/**
 * Atomically updates the current value with the results of
 * applying the given function, returning the previous value. The
 * function should be side-effect-free, since it may be re-applied
 * when attempted updates fail due to contention among threads.
 *
 * @param updateFunction a side-effect-free function
 * @return the previous value
 * @since 1.8
 */
public final int getAndUpdate(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get();
        next = updateFunction.applyAsInt(prev);
        // 判断prev和内存中值是否一致,如果一致则更改并返回true,否则一直循环
    } while (!compareAndSet(prev, next));
    return prev;
}

面试题:以CAS为基础实现一个自旋锁

java">package com.lc.test04;

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

/**
 * @author liuchao
 * @date 2023/4/16
 */
public class CasTest01 {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    /**
     * 加锁
     */
    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "进入加锁");
        //如果当前atomicReference变量中的值为空,说明无线程使用,则当前线程占用加锁成功
        while (!atomicReference.compareAndSet(null, thread)) {

        }
    }

    /**
     * 解锁
     */
    public void unlock() {
        Thread thread = Thread.currentThread();
        //如果当前atomicReference变量中的值为当前线程,则将内存值设置为null,让别的线程使用,解锁成功
        atomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + "解锁");
    }

    public static void main(String[] args) {
        CasTest01 cas = new CasTest01();
        new Thread(() -> {
            cas.lock();
            try {
                //模拟业务执行5秒
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                cas.unlock();
            }

        }, "t1").start();


        //让线程t1 先于t2 执行
        try {
            TimeUnit.MILLISECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(() -> {
            cas.lock();
            try {
                //模拟业务执行5秒
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                cas.unlock();
            }
        }, "t2").start();
    }

}

t1进入加锁
t2进入加锁
t1解锁
t2解锁

5、CAS缺点

两个问题:

  • 循环时间长开销大

在并发线程数少的时候看不出来,随着线程数的不断增大,因为只有一个线程能拿到锁其他线程一直在自璇,会明显感觉出来,所以在高并发场景不建议使用CAS。

CAS算法实现的一个重要前提是需要取出内存中某个时刻的数据并在当下时刻比较并替换,那么在这个时间差内会数据可能从A变成B,然后又变成A了,这个时候进行CAS操作会发现内存中任然是A,期望值和内存值是一致的,然后就操作成功。 

尽管CAS操作成功了,但是不代表这个过程就是没有问题的。

举个现实中的问题,我们看过很多影视片中,银行每隔一段时间都要比对一下账户的钱是正确,假如是3个月,那财务主管在第一次比对账户后,将银行钱挪用100万,然后等下次比对账户之前再填充上去,第二次比对账户的时候钱总额没有问题,但是中间却出现了公款私用问题。

那要怎么解决此问题呢?

为了解决此问题引入了AtomicStampedReference类,此类针对每次修改都会将版本号加1,使用如下

java">       /**
         * 参数1:初始化值
         * 参数2:初始化版本号
         */
        AtomicStampedReference<Long> atomicStampedReference = new AtomicStampedReference<>(1L, 1);

        System.out.println(String.format("初始值:%s,初始版本号:%s", atomicStampedReference.getReference(), atomicStampedReference.getStamp()));

        /**
         * 比较并设置
         * 参数1:期望值
         * 参数2:要修改为的值
         * 参数3:期望版本号
         * 参数4:要修改的版本号
         */
        boolean result = atomicStampedReference.compareAndSet(1L, 2L, atomicStampedReference.getStamp(),
                atomicStampedReference.getStamp() + 1);


        System.out.println(String.format("第一次执行结果:%s,值:%s,版本号:%s", result, atomicStampedReference.getReference(), atomicStampedReference.getStamp()));

        /**
         * 中间又将值改成初始值了
         */
        result = atomicStampedReference.compareAndSet(2L, 1L, atomicStampedReference.getStamp(),
                atomicStampedReference.getStamp() + 1);


        System.out.println(String.format("第二次执行结果:%s,值:%s,版本号:%s", result, atomicStampedReference.getReference(), atomicStampedReference.getStamp()));

初始值:1,初始版本号:1
第一次执行结果:true,值:2,版本号:2
第二次执行结果:true,值:1,版本号:3


http://www.niftyadmin.cn/n/231473.html

相关文章

《论文阅读》Unified Named Entity Recognition as Word-Word Relation Classification

总结 将NER视作是word-word间的 Relation Classification。 这个word-word 间的工作就很像是TPlinker那个工作&#xff0c;那篇工作是使用token间的 link。推荐指数&#xff1a;★★★☆☆值得学习的点&#xff1a; &#xff08;1&#xff09;用关系抽取的方法做NER抽取 &…

深入浅出JS定时器:从setTimeout到setInterval

前言 当谈到 JavaScript 编程语言最基本的概念时&#xff0c;定时器就是一个必须掌握的知识点。在编写网站时&#xff0c;你经常会遇到需要在一定时间间隔内执行一些代码的情况。这时候&#xff0c;JavaScript 定时器就可以派上用场了。 什么是定时器&#xff1f; JS 定时器是…

Canal(2):Canal 部署与配置

1 mysql数据库配置 1.1新建数据库 1.2 新建测试表 CREATE TABLE user_info(id VARCHAR(255),name VARCHAR(255),sex VARCHAR(255) ); 1.3 修改配置文件开启 Binlog vim /etc/my.cnf server-id4 log-binmysql-bin binlog_formatrow binlog-do-dbcaneltestdb 1.4 重启MySQL使配…

不可分解分布(Indecomposable distribution)与无限可分性(infinite divisibility)

文章目录1. 不可分解分布&#xff08;Indecomposable distribution&#xff09;1.1 定义1.2 例子1.2.1 不可分解&#xff08;Indecomposable&#xff09;1.2.2 可分解1.3 相关概念2. 无限可分性&#xff08;Infinite divisibility&#xff09;&#xfffd; )林 &#xfffd; →…

sscanf和snprintf格式化时间字符串的日期与时间戳相互转换用法

sscanf格式化时间字符串的用法 UTC&#xff1a;Coordinated Universal Time 协调世界时。因为地球自转越来越慢&#xff0c;每年都会比前一年多出零点几秒&#xff0c;每隔几年协调世界时组织都会给世界时1秒&#xff0c;让基于原子钟的世界时和基于天文学&#xff08;人类感知…

每天一道大厂SQL题【Day21】华泰证券真题实战(三)

每天一道大厂SQL题【Day21】华泰证券真题实战(三) 大家好&#xff0c;我是Maynor。相信大家和我一样&#xff0c;都有一个大厂梦&#xff0c;作为一名资深大数据选手&#xff0c;深知SQL重要性&#xff0c;接下来我准备用100天时间&#xff0c;基于大数据岗面试中的经典SQL题&…

Debezium同步之Debezium Ui界面

目录 前言 安装和配置 配置 Debezium 用户界面 Debezium UI 容器图像 自包含示例 界面操作 UI 连接器列表

MyBatis学习总结(五)逆向工程

MyBatis学习总结&#xff08;五&#xff09;逆向工程 一、MyBatis的逆向工程 正向工程&#xff1a;先创建Java实体类&#xff0c;由框架负责根据实体类生成数据库表。 Hibernate是支持正向工 程的。逆向工程&#xff1a;先创建数据库表&#xff0c;由框架负责根据数据库表&am…