并发编程——CAS

news2024/11/24 22:59:45

如果有兴趣了解更多相关内容的话,可以来我的个人网站看看:耶瞳空间

一:前言

首先看一个案例:我们开发一个网站,需要对访问量进行统计,用户每发送一次请求,访问量+1,如何实现?我们模拟有100个人同时访问,并且每个人对网站发起10次请求,最后总访问次数应该是1000次。

模拟代码如下:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Demo {
    // 总访问量
    static int count = 0;

    // 模拟访问的方法
    public static void request() throws InterruptedException {
        // 模拟耗时5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        // 开始时间
        long startTime = System.currentTimeMillis();
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);

        for (int i = 0; i < threadSize; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 模拟用户行为,每个用户访问10次网站
                    try {
                        for (int j = 0; j < 10; j++) {
                            request();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        countDownLatch.countDown();
                    }
                }
            });
            thread.start();
        }
        // 利用countDownLatch实现100个线程结束之后再执行以下代码
        countDownLatch.await();
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ", 耗时:" + (endTime - startTime) + ", count = " + count);
    }
}

在这里插入图片描述

很明显count不是1000,那么问题出在哪呢?

对于代码中的count++,其实是分这样几个步骤执行的:

  1. 获取count的值,记作A:A=count
  2. 将A值+1,得到B:B=A+1
  3. 将B值赋值给count

如果有A、B这两个线程同时执行count++,他们通知执行到上面步骤的第一步,得到的count是一样的,那么3步操作结束后,count只加了1,导致count结果不正确

那么怎么解决这个问题呢?可以在对count++操作的时候,我们让多个线程排队处理,多个线程同时到达request()方法的时候,只能允许一个线程进去操作,其他的线程在外面等待,等里面的处理完毕出来之后,外面等着的再进去一个,这样操作的count++就是排队进行的,结果一定是正确的

怎么实现排队效果呢?java中synchronized关键字和ReentrantLock都可以实现对资源加锁,保证并发正确性,多线程的情况下可以保证被锁住的资源被串行访问

在代码中就是在request()方法里加synchronized修饰,具体可见我的另一篇博客:并发编程——共享模型之管程

在这里插入图片描述
在这里插入图片描述
可见上锁之后,访问量就正常了,但也注意到,耗时从63ms变成了5335ms,那是因为在方法上加锁,导致TimeUnit.MILLISECONDS.sleep(5);也被串行执行了,因此仅仅等待就花费了5000ms,而结果执行错误只是因为count++被并发访问,因此我们可以只给count++加锁:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Demo {
    // 总访问量
    volatile static int count = 0;

    /**
     * @param expectCount 期望值count
     * @param newCount 需要给count赋值的新值
     * @return boolean
     */
    public static synchronized boolean compareAndSwap(int expectCount, int newCount) {
        // 判断count当前值是否和期望值expectCount一致,如果一致,则将newCount赋值给count
        if(getCount() == expectCount) {
            count = newCount;
            return true;
        }
        return false;
    }

    public static int getCount() {
        return count;
    }

    // 模拟访问的方法
    public static void request() throws InterruptedException {
        // 模拟耗时5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        int expectCount; // 表示期望值
        while(!compareAndSwap(expectCount = getCount(), expectCount + 1)) { }
    }

    public static void main(String[] args) throws InterruptedException {
        // 开始时间
        long startTime = System.currentTimeMillis();
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);

        for (int i = 0; i < threadSize; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 模拟用户行为,每个用户访问10次网站
                    try {
                        for (int j = 0; j < 10; j++) {
                            request();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        countDownLatch.countDown();
                    }
                }
            });
            thread.start();
        }
        // 利用countDownLatch实现100个线程结束之后再执行以下代码
        countDownLatch.await();
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ", 耗时:" + (endTime - startTime) + ", count = " + count);
    }
}

很明显这样程序的效率得到了很大的提升
在这里插入图片描述

二:CAS介绍

那现在回到正题,CAS全称“Compare And Swap”,即“比较并替换”。CAS操作包含三个操作数:

  • V:要更新的变量(var)
  • E:预期值(expected)
  • N:新值(new)

比较并交换的过程如下:判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。所以这里的预期值E本质上指的是“旧值”。

我们以一个简单的例子来解释这个过程:

  1. 如果有一个多线程共享的变量i原本等于5,我现在在线程A中,想把它设置为新的值6,我们使用CAS来做这个事情。
  2. 首先我们用i去与5对比,发现它等于5,说明没有被其它线程改过,那我就把它设置为新的值6,此次CAS成功,i的值被设置成了6;
  3. 如果不等于5,说明i被其它线程改过了(比如现在i的值为2),那么我就什么也不做,此次CAS失败,i的值仍为2。

在这个例子中, i就是V,5就是E,6就是N。

那有没有可能我在判断了i为5之后,正准备更新它的新值的时候,被其它线程更改了i的值呢?不会的。因为CAS是一种原子操作,它是一种系统原语,是一条CPU的原子指令,从CPU层面保证它的原子性。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,这个重试的过程也叫做自旋。当然也允许失败的线程放弃操作。正是基于这样的原理,CAS即时没有使用锁,也能发现其他线程对当前线程的干扰,从而进行及时的处理。

CAS的缺点:

  • CPU开销较大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
  • 不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

三:基本使用

java提供了对CAS操作的支持,现在用CAS修改上面的例题:

public class Demo {
  // 总访问量
  static AtomicInteger count = new AtomicInteger(0);

  // 模拟访问的方法
  public static void request() throws InterruptedException {
    // 模拟耗时5毫秒
    TimeUnit.MILLISECONDS.sleep(5);
    // 修改访问量
    while (true) {
      int oldCount = count.get();
      int newCount = oldCount + 1;
      if (count.compareAndSet(oldCount, newCount)) {
        break;
      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    long startTime = System.currentTimeMillis();
    int threadSize = 100;
    CountDownLatch countDownLatch = new CountDownLatch(threadSize);

    for (int i = 0; i < threadSize; i++) {
      Thread thread = new Thread(() -> {
        // 模拟用户行为,每个用户访问10次网站
        try {
          for (int j = 0; j < 10; j++) {
            request();
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          countDownLatch.countDown();
        }
      });
      thread.start();
    }
    // 利用countDownLatch实现100个线程结束之后再执行以下代码
    countDownLatch.await();
    long endTime = System.currentTimeMillis();
    System.out.println(Thread.currentThread().getName() + ", 耗时:" + (endTime - startTime) + ", count = " + count);
  }
}

不过需要注意的是,在获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。volatile可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,而是到主存中获取它的值,线程操作volatile变量都是直接操作主存。即一个线程对volatile变量的修改,对另一个线程可见。CAS必须借助volatile才能读取到共享变量的最新值来实现比较并交换的效果。关于volatile,详细介绍可以看我的这篇博客:并发编程——可见性与有序性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/392484.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端都在聊什么 - 第 4 期

Hello 小伙伴们早上、中午、下午、晚上、深夜好&#xff0c;我是爱折腾的 jsliang~「前端都在聊什么」是 jsliang 日常写文章/做视频/玩直播过程中&#xff0c;小伙伴们的提问以及我的解疑整理。本文章视频同步&#xff1a;TODO:本期对应 2023.01.28 当天直播间的粉丝互动。主要…

关于Scipy的概念和使用方法及实战

关于scipy的概念和使用方法 什么是Scipy Scipy是一个基于Python的科学计算库&#xff0c;它提供了许多用于数学、科学、工程和技术计算的工具和函数。Scipy的名称是“Scientific Python”的缩写。 Scipy包含了许多子模块&#xff0c;其中一些主要的子模块包括&#xff1a; …

eBPF(内核态)和WebAssembly

1 什么是eBPF 无需修改内核&#xff0c;也不用加载内核模块&#xff0c;程序员就可以在内核中执行执行自定义的字节码。 eBPF&#xff0c;它的全称是“Extended Berkeley Packet Filter”&#xff0c; 网络数据包过滤模块。我们很熟悉的 tcpdump 工具&#xff0c;它就是利用了…

Bellman-ford和SPFA算法

目录 一、前言 二、Bellman-ford算法 1、算法思想 2、算法复杂度 3、判断负圈 4、出差&#xff08;2022第十三届国赛&#xff0c;lanqiaoOJ题号2194&#xff09; 三、SPFA算法&#xff1a;改进的Bellman-Ford 1、随机数据下的最短路问题&#xff08;lanqiaoOJ题号1366&…

xss靶场绕过

目录 第一关 原理 payload 第二关 原理 payload 第三关 原理 payload 第四关 原理 payload 第五关 原理 payload 第六关 原理 payload 第七关 原理 payload 第八关 原理 payload 第九关 原理 payload 第十关 原理 payload 第十一关 原理 payl…

Ubuntu 虚拟机 安装nvidia驱动失败,进不了系统

VMware 安装的 Ubuntu 1804 安装 英伟达显卡失败后&#xff0c;启动出现&#xff1a;在上面那个页面&#xff0c;直接使用组合键&#xff1a;Ctrl Alt F3 便可以进入命令行模式。如果可以成功进入&#xff0c;则说明ubantu系统确实起来了&#xff0c;只是界面相关的模块没有成…

Win32api学习之常见编码格式(一)

ASCII编码 ASCII编码是一种最早出现的字符编码方案&#xff0c;它是由美国标准化协会&#xff08;ASA&#xff09;于1963年制定的标准&#xff0c;用于在计算机系统中表示英语文本字符集。ASCII编码仅使用7位二进制数&#xff08;共128个&#xff09;&#xff0c;用于表示英文…

技术开发|电动充气泵控制方案

电动充气泵是为了保障汽车车胎对汽车的行驶安全所配备的&#xff0c;防止遇上紧急问题时没有解决方案&#xff0c;同时也可以检测轮胎胎压。电动充气泵方案具有多种充气模式设定&#xff0c;小到篮球大到汽车等都能够满足其充气需求&#xff0c;共有四种模式充气&#xff0c;分…

Solidworks导出URDF总结(Humble)

环境 Solidwoks2021 SP5&#xff1b;Ubuntu22.04&#xff1b;ROS2 Humble; 步骤 基本步骤参考&#xff1a;Solidworks导出URDF总结&#xff08;Noetic&#xff09; 本文只介绍不同之处。 将solidworks生成的文件夹&#xff08;我这里为wuwei2&#xff09;移动到/ws_moveit2…

数位DP

数位dp的题目一般会问&#xff0c;某个区间内&#xff0c;满足某种性质的数的个数。 利用前缀和&#xff0c;比如求区间[l,r]中的个数&#xff0c;转化成求[0,r]的个数 [0,l-1]的个数。利用树的结构来考虑&#xff08;按位分类讨论&#xff09; 1081. 度的数量 #include<…

全网最全之接口测试【加密解密攻防完整版】实战教程详解

看视频讲的更详细&#xff1a;https://www.bilibili.com/video/BV1zr4y1E7V5/? 一、对称加密 对称加密算法是共享密钥加密算法&#xff0c;在加密解密过程中&#xff0c;使用的密钥只有一个。发送和接收双方事先都知道加密的密钥&#xff0c;均使用这个密钥对数据进行加密和解…

RedFish模拟仿真调试

前一段时间在真机上调试Redfish&#xff0c;自己一直想把模拟仿真给调试出来&#xff0c;想通过debug看看全部的流程细节&#xff0c;有空了也能自己玩玩&#xff0c;在github上RedfishPkg的页面有具体的说明&#xff0c;想看的移步&#xff1a; edk2-staging/RedfishPkg at e…

使用Vue实现数据可视化大屏功能(一)

导语   现在在很多的工程项目中&#xff0c;都有有关于数据大屏相关的监控内容&#xff0c;这里我们就来看一下如何用Vue来搭建一个数据可视化大屏应用。 创建项目 使用WebStorm工具创建一个Vue的项目。如下图所示&#xff0c;配置好vue的脚手架工具和nodejs的运行环境&#…

Httpclient测试

在IDEA中有一个非常方便的http接口测试工具httpclient&#xff0c;下边介绍它的使用方法&#xff0c;后边我们会用它进行接口测试。如果IDEA版本较低没有自带httpclient&#xff0c;需要安装httpclient插件1.插件2.controller进入controller类&#xff0c;找到http接口对应的方…

Unity - 搬砖日志 - BRP 管线下的自定义阴影尺寸(脱离ProjectSettings/Quality/ShadowResolution设置)

文章目录环境原因解决CSharp 脚本效果预览 - Light.shadowCustomResolution效果预览 - Using Quality Settings应用ControlLightShadowResolution.cs ComponentTools Batching add the Component to all LightReferences环境 Unity : 2020.3.37f1 Pipeline : BRP 原因 (好久没…

JVM系统优化实践(8):订单系统的垃圾回收案例

您好&#xff0c;我是湘王&#xff0c;这是我的CSDN博客&#xff0c;欢迎您来&#xff0c;欢迎您再来&#xff5e;上回说到了年轻代和老年代的两个垃圾回收器&#xff1a;ParNew和CMS&#xff0c;并且将CMS的GC过程也一并介绍了&#xff0c;现在来看个订单系统的案例。假设有这…

常见数据结构

一. 数据结构概述、栈、队列 1. 数据结构概述 2. 栈数据结构的执行特点 3. 常见数据结构之队列 二. 常见数据结构之数组 数组它就是内存中的一块儿连续区域。数组变量存的是数组在堆内存当中的起始地址。数组查询任意索引位置的值耗时相同&#xff0c;数组根据索引查询速度快。…

Matlab中旧版modem.qammod与新版不兼容

最近&#xff0c;因为课题需要&#xff0c;在研究通信。在网上下了一个2015年左右的代码&#xff0c;其中用的是matlab旧版中的modem.qammod函数&#xff0c;但是旧版中的函数已经被删除了&#xff0c;&#xff08;这里必须得吐槽一下&#xff0c;直接该函数内部运行机制就行呀…

Lasso回归理论及代码实现

Lasso回归的模型可以写作与一般线性回归相比, Lasso回归加入了回归项系数的一范数, 这样做是为了防止线性回归过程发生的过拟合现象. 直观点看, 其将的分量限制在了一个以圆点为中心以为边的正方形内. 与岭回归相比, 该模型得到的系数矩阵更为稀疏. 由于函数在0点不可导, 因而L…

第二章Linux操作语法1

文章目录vi和vim常用的三种模式vi和vim快捷键Linux开机&#xff0c;重启用户管理用户信息查询管理who和whoami用户组信息查询管理用户和组的相关文件实用指令集合运行级别帮助指令manhelp文件管理类pwd命令ls命令cd命令mkdir命令rmdir命令rm命令touch命令cp指令mv指令文件查看类…