1.概述

转载:添加链接描述

  1. 从网站计数器实现中一步步引出CAS操作

  2. 介绍java中的CAS及CAS可能存在的问题

  3. 悲观锁和乐观锁的一些介绍及数据库乐观锁的一个常见示例

  4. 使用java中的原子操作实现网站计数器功能

1.1 我们需要解决的问题

需求:我们开发了一个网站,需要对访问量进行统计,用户每次发一次请求,访问量+1,如何实现呢?

下面我们来模仿有100个人同时访问,并且每个人对咱们的网站发起10次请求,最后总访问次数应该是1000次。实现访问如下。

package com.java.thread.demo.cas;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;/*** 跟着阿里p7学并发,微信公众号:javacode2018*/
public class CasDemo1 {//访问次数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 starTime = System.currentTimeMillis();int threadSize = 100;CountDownLatch countDownLatch = new CountDownLatch(threadSize);for (int i = 0; i < threadSize; i++) {Thread thread = new Thread(() -> {try {for (int j = 0; j < 10; j++) {request();}} catch (InterruptedException e) {e.printStackTrace();} finally {countDownLatch.countDown();}});thread.start();}countDownLatch.await();long endTime = System.currentTimeMillis();System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);}
}

结果如下

main,耗时:167,count=979

代码中的count用来记录总访问次数,request()方法表示访问一次,内部休眠5毫秒模拟内部耗时,request方法内部对count++操作。程序最终耗时1秒多,执行还是挺快的,但是count和我们期望的结果不一致,我们期望的是1000,实际输出的是973(每次运行结果可能都不一样)。

分析一下问题出在哪呢?

代码中采用的是多线程的方式来操作count,count++会有线程安全问题,count++操作实际上是由以下三步操作完成的:

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

如果有A、B两个线程同时执行count++,他们同时执行到上面步骤的第1步,得到的count是一样的,3步操作完成之后,count只会+1,导致count只加了一次,从而导致结果不准确。

那么我们应该怎么做的呢

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

我们前面学了synchronized、ReentrantLock可以对资源加锁,保证并发的正确性,多线程情况下可以保证被锁的资源被串行访问,那么我们用synchronized来实现一下。

1.1.1 使用synchronized实现

package com.java.thread.demo.cas;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;/*** 跟着阿里p7学并发,微信公众号:javacode2018*/
public class Demo2 {//访问次数static int count = 0;//模拟访问一次public static synchronized void request() throws InterruptedException {//模拟耗时5毫秒TimeUnit.MILLISECONDS.sleep(5);count++;}public static void main(String[] args) throws InterruptedException {long starTime = System.currentTimeMillis();int threadSize = 100;CountDownLatch countDownLatch = new CountDownLatch(threadSize);for (int i = 0; i < threadSize; i++) {Thread thread = new Thread(() -> {try {for (int j = 0; j < 10; j++) {request();}} catch (InterruptedException e) {e.printStackTrace();} finally {countDownLatch.countDown();}});thread.start();}countDownLatch.await();long endTime = System.currentTimeMillis();System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);}
}

输出

main,耗时:5971,count=1000

程序中request方法使用synchronized关键字,保证了并发情况下,request方法同一时刻只允许一个线程访问,request加锁了相当于串行执行了,count的结果和我们预期的结果一致,只是耗时比较长,5秒多。

1.1.2 方式3

我们在看一下count++操作,count++操作实际上是被拆分为3步骤执行:

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

方式2中我们通过加锁的方式让上面3步骤同时只能被一个线程操作,从而保证结果的正确性。

我们是否可以只在第3步加锁,减少加锁的范围,对第3步做以下处理:

获取锁
第三步获取一下count最新的值,记做LV
判断LV是否等于A,如果相等,则将B的值赋给count,并返回true,否者返回false
释放锁

如果我们发现第3步返回的是false,我们就再次去获取count,将count赋值给A,对A+1赋值给B,然后再将A、B的值带入到上面的过程中执行,直到上面的结果返回true为止。

我们用代码来实现,如下:

package com.java.thread.demo.cas;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;/*** 跟着阿里p7学并发,微信公众号:javacode2018*/
public class Demo3 {//访问次数volatile static int count = 0;//模拟访问一次public static void request() throws InterruptedException {//模拟耗时5毫秒TimeUnit.MILLISECONDS.sleep(5);int expectCount;do {expectCount = getCount();} while (!compareAndSwap(expectCount, expectCount + 1));}/*** 获取count当前的值** @return*/public static int getCount() {return count;}/*** @param expectCount 期望count的值* @param newCount    需要给count赋的新值* @return*/public static synchronized boolean compareAndSwap(int expectCount, int newCount) {//判断count当前值是否和期望的expectCount一样,如果一样将newCount赋值给countif (getCount() == expectCount) {count = newCount;return true;}return false;}public static void main(String[] args) throws InterruptedException {long starTime = System.currentTimeMillis();int threadSize = 100;CountDownLatch countDownLatch = new CountDownLatch(threadSize);for (int i = 0; i < threadSize; i++) {Thread thread = new Thread(() -> {try {for (int j = 0; j < 10; j++) {request();}} catch (InterruptedException e) {e.printStackTrace();} finally {countDownLatch.countDown();}});thread.start();}countDownLatch.await();long endTime = System.currentTimeMillis();System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);}
}

输出:

main,耗时:116,count=1000

代码中用了volatile关键字修饰了count,可以保证count在多线程情况下的可见性。关于volatile关键字的使用,也是非常非常重要的,前面有讲过,不太了解的朋友可以去看一下:volatile与Java内存模型

咱们再看一下代码,compareAndSwap方法,我们给起个简称吧叫CAS,这个方法有什么作用呢?这个方法使用synchronized修饰了,能保证此方法是线程安全的,多线程情况下此方法是串行执行的。方法由两个参数,expectCount:表示期望的值,newCount:表示要给count设置的新值。方法内部通过getCount()获取count当前的值,然后与期望的值expectCount比较,如果期望的值和count当前的值一致,则将新值newCount赋值给count。

再看一下request()方法,方法中有个do-while循环,循环内部获取count当前值赋值给了expectCount,循环结束的条件是compareAndSwap返回true,也就是说如果compareAndSwap如果不成功,循环再次获取count的最新值,然后+1,再次调用compareAndSwap方法,直到compareAndSwap返回成功为止。

代码中相当于将count++拆分开了,只对最后一步加锁了,减少了锁的范围,此代码的性能是不是比方式2快不少,还能保证结果的正确性。大家是不是感觉这个compareAndSwap方法挺好的,这东西确实很好,java中已经给我们提供了CAS的操作,功能非常强大,我们继续向下看。

2.CAS

CAS,compare and swap的缩写,中文翻译成比较并交换。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

很多地方说CAS操作是非阻塞的,其实系统底层进行CAS操作的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,所以同一芯片上的其他处理器就暂时不能通过总线访问内存,保证了该指令在多处理器环境下的原子性。总线上锁的,其他线程执行CAS还是会被阻塞一下,只是时间可能会非常短暂,所以说CAS是非阻塞的并不正确,只能说阻塞的时间是非常短的。

java中提供了对CAS操作的支持,具体在sun.misc.Unsafe类中,声明如下:

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

上面三个方法都是类似的,主要对4个参数做一下说明。

var1:表示要操作的对象var2:表示要操作对象中属性地址的偏移量var4:表示需要修改数据的期望的值var5:表示需要修改为的新值

JUC包中大部分功能都是依靠CAS操作完成的,所以这块也是非常重要的,有关Unsafe类,下篇文章会具体讲解。

synchronizedReentrantLock这种独占锁属于悲观锁,它是在假设需要操作的代码一定会发生冲突的,执行代码的时候先对代码加锁,让其他线程在外面等候排队获取锁。悲观锁如果锁的时间比较长,会导致其他线程一直处于等待状态,像我们部署的web应用,一般部署在tomcat中,内部通过线程池来处理用户的请求,如果很多请求都处于等待获取锁的状态,可能会耗尽tomcat线程池,从而导致系统无法处理后面的请求,导致服务器处于不可用状态。

除此之外,还有乐观锁,乐观锁的含义就是假设系统没有发生并发冲突,先按无锁方式执行业务,到最后了检查执行业务期间是否有并发导致数据被修改了,如果有并发导致数据被修改了 ,就快速返回失败,这样的操作使系统并发性能更高一些。cas中就使用了这样的操作。

关于乐观锁这块,想必大家在数据库中也有用到过,给大家举个例子,可能以后会用到。

如果你们的网站中有调用支付宝充值接口的,支付宝那边充值成功了会回调商户系统,商户系统接收到请求之后怎么处理呢?假设用户通过支付宝在商户系统中充值100,支付宝那边会从用户账户中扣除100,商户系统接收到支付宝请求之后应该在商户系统中给用户账户增加100,并且把订单状态置为成功。

处理过程如下:

开启事务
获取订单信息
if(订单状态==待处理){给用户账户增加100将订单状态更新为成功
}
返回订单处理成功
提交事务

由于网络等各种问题,可能支付宝回调商户系统的时候,回调超时了,支付宝又发起了一笔回调请求,刚好这2笔请求同时到达上面代码,最终结果是给用户账户增加了200,这样事情就搞大了,公司蒙受损失,严重点可能让公司就此倒闭了。

那我们可以用乐观锁来实现,给订单表加个版本号version,要求每次更新订单数据,将版本号+1,那么上面的过程可以改为:

获取订单信息,将version的值赋值给V_A
if(订单状态==待处理){开启事务给用户账户增加100update影响行数 = update 订单表 set version = version + 1 where id = 订单号 and version = V_A;if(update影响行数==1){提交事务}else{回滚事务}
}
返回订单处理成功

上面的update语句相当于我们说的CAS操作,执行这个update语句的时候,多线程情况下,数据库会对当前订单记录加锁,保证只有一条执行成功,执行成功的,影响行数为1,执行失败的影响行数为0,根据影响行数来决定提交还是回滚事务。上面操作还有一点是将事务范围缩小了,也提升了系统并发处理的性能。这个知识点希望你们能get到。

3.CAS 的问题

cas这么好用,那么有没有什么问题呢?还真有

ABA问题

CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。目前在JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长开销大

上面我们说过如果CAS不成功,则会原地循环(自旋操作),如果长时间自旋会给CPU带来非常大的执行开销。并发量比较大的情况下,CAS成功概率可能比较低,可能会重试很多次才会成功。

4.使用JUC中的类实现计数器

juc框架中提供了一些原子操作,底层是通过Unsafe类中的cas操作实现的。通过原子操作可以保证数据在并发情况下的正确性。

此处我们使用java.util.concurrent.atomic.AtomicInteger类来实现计数器功能,AtomicInteger内部是采用cas操作来保证对int类型数据增减操作在多线程情况下的正确性。

计数器代码如下:

计数器代码如下:

package com.itsoku.chat20;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;/*** 跟着阿里p7学并发,微信公众号:javacode2018*/
public class Demo4 {//访问次数static AtomicInteger count = new AtomicInteger();//模拟访问一次public static void request() throws InterruptedException {//模拟耗时5毫秒TimeUnit.MILLISECONDS.sleep(5);//对count原子+1count.incrementAndGet();}public static void main(String[] args) throws InterruptedException {long starTime = System.currentTimeMillis();int threadSize = 100;CountDownLatch countDownLatch = new CountDownLatch(threadSize);for (int i = 0; i < threadSize; i++) {Thread thread = new Thread(() -> {try {for (int j = 0; j < 10; j++) {request();}} catch (InterruptedException e) {e.printStackTrace();} finally {countDownLatch.countDown();}});thread.start();}countDownLatch.await();long endTime = System.currentTimeMillis();System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);}
}

输出:

main,耗时:119,count=1000

耗时很短,并且结果和期望的一致。

关于原子类操作,都位于java.util.concurrent.atomic包中,下篇文章我们主要来介绍一下这些常用的类及各自的使用场景。

【高并发】java中的CAS,你需要知道的东西相关推荐

  1. 【面试篇】Java多线程并发-Java中的CAS机制算法

    Java中的CAS机制算法 a.CAS例子 再讲解CAS机制之前,先来看一道经典的并发执行1000次递增的问题: public class Test { public static int count ...

  2. JAVA 中的 CAS

    原文地址:https://www.xilidou.com/2018/02/01/java-cas/ CAS 是现代操作系统,解决并发问题的一个重要手段,最近在看 eureka 的源码的时候.遇到了很多 ...

  3. 高并发必学的 CAS 操作,看这篇就够了!

    大家好,我是树哥. CAS 操作是高并发场景下,性能如此之高的一个重要优化.今天推荐胜哥的一篇关于 CAS 的文章,带你了解 CAS 的前世今生,写得真是太棒了! 背景 在高并发的业务场景下,线程安全 ...

  4. java设计模式并发_[高并发Java 七] 并发设计模式

    [高并发Java 七] 并发设计模式 [高并发Java 七] 并发设计模式 为什么80%的码农都做不了架构师?>>> 在软件工程中,设计模式(design pattern)是对软件设 ...

  5. 高并发系统中的限流应该如何做?

    缓存 缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪. 使用缓存不单单能够提升系统访问速度.提高并发访问量,也是保护数据库.保护系统的有效方式.大型网站一般主要是 ...

  6. 高并发分布式系统中生成全局唯一Id汇总

    全局唯一ID <高并发分布式系统中生成全局唯一Id汇总> Twitter 方案(Snowflake 算法):41位时间戳+10位机器标识(比如IP,服务器名称等)+12位序列号(本地计数器 ...

  7. OkHttp工具类在微服高并发场景中问题实践总结

    OkHttp工具类在微服高并发场景中问题实践总结 问题场景 我的应用是一个中间业务应用XXApp,一个交易请求进来需要依赖下游应用,采用http协议通讯方式,需要调用3-4次下游请求. 老XXApp在 ...

  8. Java中的CAS操作

    Java中的CAS的含义 CAS即是Compare and Swap ,它是JDK提供的非阻塞原子性操作,它通过硬件保证了比较一更新操作的原子性.CAS 操作包含三个操作数-内存位置(V).预期原值( ...

  9. Java中的CAS以及AQS实现原理

    Java中的CAS实现原理 什么是CAS? 在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令. 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将 ...

最新文章

  1. 第五届蓝桥杯 c/c++ B组6
  2. Chipseq数据库的建立
  3. ThinkPhp5使用bootstrap样式分页
  4. Hadoop实战-中高级部分 之 Hadoop 集群安装
  5. 《HBase权威指南》一导读
  6. uva 10673 ——Play with Floor and Ceil
  7. 红黑树插入和删除的各种情况分析
  8. python语法基础整理_Python基础语法笔记整理——只记录部分不熟少用的
  9. 企业知识管理与协同软件Confluence搭建与配置
  10. 面向对象3:类和对象
  11. Scratch编程与科学结合-串联与并联
  12. WPF利用NotifyIcon创建任务栏图标(菜鸟教程)
  13. 500次 “LOVE“的歌词 Taylor Swift歌词数据可视化分享
  14. 回顾 | Tencent Serverless Hours 线上分享会第一期
  15. MFC之对于文档类的DeleteContents和OnNewDocument说明29
  16. ICCV, ECCV, CVPR,IEEE的关系
  17. python pywin32 的学习笔记
  18. 【AI 2021】Adobe Illustrator 2021 软件下载及安装教程
  19. Python 1-10 字符串操作
  20. 【线段树】[LUOGU 守墓人] [LUOGU 维护序列] 线段树模板题

热门文章

  1. 三星Galaxy S21纪念版开启预售:“海洋蓝”配色+金色中框
  2. 苹果出来挨打!又一个不配充电器的来了
  3. 京东健康上线“新冠病毒”核酸检测 在线预约服务
  4. 外媒:华为或将在2月24日发布麒麟820 支持5G网络
  5. 高通孟樸:5G+AI将会是移动互联网主流趋势
  6. 简直没法玩!iOS 13三指手势影响多款游戏操作,腾讯建议玩家谨慎更新
  7. 再等等!华为折叠屏手机Mate X预计在7月底至8月初开售
  8. 华为nova 5 Pro现身GeekBench数据库:妥妥麒麟980水准
  9. 支付宝蚂蚁森林入选2019年世界环境日实践案例
  10. 苹果涉嫌利用App Store打压屏幕时间应用竞争对手 已被投诉至欧盟