并发编程——CAS
创始人
2024-05-29 16:58:09
0

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

一:前言

首先看一个案例:我们开发一个网站,需要对访问量进行统计,用户每发送一次请求,访问量+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() {@Overridepublic 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赋值给countif(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() {@Overridepublic 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,详细介绍可以看我的这篇博客:并发编程——可见性与有序性

相关内容

热门资讯

122.(leaflet篇)l... 听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
Vue使用pdf-lib为文件... 之前也写过两篇预览pdf的,但是没有加水印,这是链接:Vu...
PyQt5数据库开发1 4.1... 文章目录 前言 步骤/方法 1 使用windows身份登录 2 启用混合登录模式 3 允许远程连接服...
Android studio ... 解决 Android studio 出现“The emulator process for AVD ...
Linux基础命令大全(上) ♥️作者:小刘在C站 ♥️个人主页:小刘主页 ♥️每天分享云计算网络运维...
再谈解决“因为文件包含病毒或潜... 前面出了一篇博文专门来解决“因为文件包含病毒或潜在的垃圾软件”的问题,其中第二种方法有...
南京邮电大学通达学院2023c... 题目展示 一.问题描述 实验题目1 定义一个学生类,其中包括如下内容: (1)私有数据成员 ①年龄 ...
PageObject 六大原则 PageObject六大原则: 1.封装服务的方法 2.不要暴露页面的细节 3.通过r...
【Linux网络编程】01:S... Socket多进程 OVERVIEWSocket多进程1.Server2.Client3.bug&...
数据结构刷题(二十五):122... 1.122. 买卖股票的最佳时机 II思路:贪心。把利润分解为每天为单位的维度,然后收...
浏览器事件循环 事件循环 浏览器的进程模型 何为进程? 程序运行需要有它自己专属的内存空间࿰...
8个免费图片/照片压缩工具帮您... 继续查看一些最好的图像压缩工具,以提升用户体验和存储空间以及网站使用支持。 无数图像压...
计算机二级Python备考(2... 目录  一、选择题 1.在Python语言中: 2.知识点 二、基本操作题 1. j...
端电压 相电压 线电压 记得刚接触矢量控制的时候,拿到板子,就赶紧去测各种波形,结...
如何使用Python检测和识别... 车牌检测与识别技术用途广泛,可以用于道路系统、无票停车场、车辆门禁等。这项技术结合了计...
带环链表详解 目录 一、什么是环形链表 二、判断是否为环形链表 2.1 具体题目 2.2 具体思路 2.3 思路的...
【C语言进阶:刨根究底字符串函... 本节重点内容: 深入理解strcpy函数的使用学会strcpy函数的模拟实现⚡strc...
Django web开发(一)... 文章目录前端开发1.快速开发网站2.标签2.1 编码2.2 title2.3 标题2.4 div和s...