JUC并发编程--------CAS、Atomic原子操作

什么是原子操作?如何实现原子操作?

什么是原子性?

事务的一大特性就是原子性(事务具有ACID四大特性),一个事务包含多个操作,这些操作要么全部执行,要么全都不执行

并发里的原子性和原子操作是一样的内涵和概念,假定有两个操作A和B都包含多个步骤,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,执行B的线程看A的操作也是一样的,那么A和B对彼此来说是原子的。

实现原子操作可以使用锁,锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,

这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。为了解决这个问题,Java提供了Atomic系列的原子操作类。

这些原子操作类其实是使用当前的处理器基本都支持CAS的指令,比如Intel的汇编指令cmpxchg,每个厂家所实现的具体算法并不一样,但是原理基本一样。每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作

CAS的基本思路 :如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。自然CAS操作执行完成时,在业务上不一定完成了,这个时候我们就会对CAS操作进行反复重试,于是就有了循环CAS。很明显,循环CAS就是在一个循环里不断的做cas操作,直到成功为止。Java中的Atomic系列的原子操作类的实现则是利用了循环CAS来实现

CAS实现原子操作的三大问题

ABA问题

因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。

循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

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

对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

Atomic系列

概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。本次我们只讲解

原理:

有3个操作数(内存值V, 旧的预期值A,要修改的值B)

当旧的预期值A == 内存值 此时修改成功,将V改为B

当旧的预期值A!=内存值 此时修改失败,不做任何操作并重新获取现在的最新值,再重复之前的方法(这个重新获取的动作就是自旋)

使用原子的方式更新基本类型,使用原子的方式更新基本类型Atomic包提供了以下3个类:

  1. AtomicBoolean: 原子更新布尔类型
  2. AtomicInteger: 原子更新整型
  3. AtomicLong: 原子更新长整型

以上3个类提供的方法几乎一模一样,所以本节仅以AtomicInteger为例进行讲解,AtomicInteger的常用方法如下:

  • public AtomicInteger():                 初始化一个默认值为0的原子型Integer
  • public AtomicInteger(int initialValue):  初始化一个指定值的原子型Integer
  • int get():                              获取值
  • int getAndIncrement():                   以原子方式将当前值加1,注意,这里返回的是自增前的值。
  • int incrementAndGet():                   以原子方式将当前值加1,注意,这里返回的是自增后的值。
  • int addAndGet(int data):                 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
  • int getAndSet(int value):                以原子方式设置为newValue的值,并返回旧值。
  •  boolean compareAndSet(int expect, int update)  于传入的expect值进行比较,如果当前值等于expect值,则更新为update。

代码实现:

package com.itheima.threadatom3;
​
import java.util.concurrent.atomic.AtomicInteger;
​
public class MyAtomIntergerDemo1 {
//    public AtomicInteger():                  初始化一个默认值为0的原子型Integer
//    public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integerpublic static void main(String[] args) {AtomicInteger ac = new AtomicInteger();System.out.println(ac);
​AtomicInteger ac2 = new AtomicInteger(10);System.out.println(ac2);}
​
}
package com.itheima.threadatom3;
​
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;
​
public class MyAtomIntergerDemo2 {
//    int get():                获取值
//    int getAndIncrement():     以原子方式将当前值加1,注意,这里返回的是自增前的值。
//    int incrementAndGet():     以原子方式将当前值加1,注意,这里返回的是自增后的值。
//    int addAndGet(int data):   以原子方式将参数与对象中的值相加,并返回结果。
//    int getAndSet(int value):  以原子方式设置为newValue的值,并返回旧值。public static void main(String[] args) {
//        AtomicInteger ac1 = new AtomicInteger(10);
//        System.out.println(ac1.get());
​
//        AtomicInteger ac2 = new AtomicInteger(10);
//        int andIncrement = ac2.getAndIncrement();
//        System.out.println(andIncrement);
//        System.out.println(ac2.get());
​
//        AtomicInteger ac3 = new AtomicInteger(10);
//        int i = ac3.incrementAndGet();
//        System.out.println(i);//自增后的值
//        System.out.println(ac3.get());
​
//        AtomicInteger ac4 = new AtomicInteger(10);
//        int i = ac4.addAndGet(20);
//        System.out.println(i);
//        System.out.println(ac4.get());
​AtomicInteger ac5 = new AtomicInteger(100);int andSet = ac5.getAndSet(20);System.out.println(andSet);System.out.println(ac5.get());}
}

对于非基本类型可以使用 AtomicReference 

代码演示:

  public static void main(String[] args) {StudentDemo studentDemo = new StudentDemo();studentDemo.setName("张三");studentDemo.setAge(18);studentDemo.setSco(100f);AtomicReference atomicReference = new AtomicReference(studentDemo);StudentDemo studentDemo2 = new StudentDemo();studentDemo2.setName("李四");Object o = atomicReference.get();studentDemo2.setName("王五");atomicReference.compareAndSet(o,studentDemo2);}

正如前面所讲过的aba问题,Atomic还提供了区别版本号的方法类

AtomicStampedReference代码演示

 public static void main(String[] args) throws InterruptedException {AtomicStampedReference<BigDecimal> bigDecimalAtomicReference = new AtomicStampedReference<>(new BigDecimal("10000"), 0);for (int i = 0; i < 1000; i++) {Thread x = new Thread(() -> {while (true) {BigDecimal bigDecimal = bigDecimalAtomicReference.getReference();BigDecimal subtract = bigDecimal.subtract(BigDecimal.TEN);int stamp = bigDecimalAtomicReference.getStamp();if (bigDecimalAtomicReference.compareAndSet(bigDecimal, subtract, stamp, stamp + 1)) {System.out.println(bigDecimalAtomicReference.getReference());break;}}});x.start();x.join();}}

 AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:
A - > B - > A - > C ,通过 AtomicStampedReference ,我们可以知道,引用变量中途被更改了几次。

但是有时候,并不关心引用变量更改了几次,只是单纯的关心 是否更改过 ,所以就有了AtomicMarkableReference

AtomicMarkableReference代码演示

  AtomicMarkableReference<BigDecimal> bigDecimalAtomicReference = new AtomicMarkableReference<>(new BigDecimal("10000"), true);for (int i = 0; i < 1000; i++) {Thread x = new Thread(() -> {while (true) {BigDecimal bigDecimal = bigDecimalAtomicReference.getReference();BigDecimal subtract = bigDecimal.subtract(BigDecimal.TEN);if (bigDecimalAtomicReference.compareAndSet(bigDecimal, subtract, true, false)) {System.out.println(bigDecimalAtomicReference.getReference());break;}}});x.start();x.join();}}

AtomicMarkableReference 提供了一个标记,在修改成功的时候把标记也一起修改,待到下一次变更的时候,如果标记改变了,则说明已经变化过值。

LongAdder

LongAdder 是并发大师 @author Doug Lea (大哥李)的作品,设计的非常精巧
 

LongAdder 类有几个关键域
// 累加单元数组 , 懒惰初始化
transient volatile Cell [] cells ;
// 基础值 , 如果没有竞争 , 则用 cas 累加这个域
transient volatile long base ;
// 在 cells 创建或扩容时 , 置为 1, 表示加锁
transient volatile int cellsBusy ;
伪共享原理// 防止缓存行伪共享
@sun.misc.Contended
static final class Cell {volatile long value;Cell(long x) { value = x; }// 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值final boolean cas(long prev, long next) {return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);}// 省略不重要代码
}


cell为累加单元,而方法上的@sun.misc.Contended 的作用在下面介绍

需要重缓存说起,在操作系统种由以下几中数据读取放方式,从cpu内部寄存器、一级缓存、二级缓存、三级缓存、内存

现在比较以下内存与缓存的数据差别


因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte ( 8 个 long )
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效


因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节( 16 字节的对象头和 8 字节的 value ),因
此缓存行可以存下 2 个的 Cell 对象。这样问题来了:
Core-0 要修改 Cell[0]
Core-1 要修改 Cell[1]
无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加
Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效
@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的
padding ,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

对于LongAdder来说,内部有一个base变量,一个Cell[]数组。

在实际运用的时候,只有从未出现过并发冲突的时候,base基数才会使用到,一旦出现了并发冲突,之后所有的操作都只针对Cell[]数组中的单元Cell。

而LongAdder最终结果的求和(如上图),并没有使用全局锁,返回值不是绝对准确的,因为调用这个方法时还有其他线程可能正在进行计数累加,所以只能得到某个时刻的近似值,这也就是LongAdder并不能完全替代LongAtomic的原因之一。

而且从测试情况来看,线程数越多,并发操作数越大,LongAdder的优势越大,线程数较小时,AtomicLong的性能还超过了LongAdder。

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

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

相关文章

ESP32之LEDC(PWM信号的输出)

一、PWM信号简介 PWM&#xff1a;脉冲宽度调制&#xff0c;简称脉宽调制频率(f)&#xff1a;一秒钟PWM有多少个周期(单位Hz)周期(T)&#xff1a;一个周期的时间占空比(duty)&#xff1a;在一个脉冲周期内&#xff0c;高电平的时间与整个周期时间的比例脉宽时间&#xff1a;一个…

Java学习笔记之----I/O(输入/输出)一

在变量、数组和对象中存储的数据是暂时存在的&#xff0c;程序结束后它们就会丢失。想要永久地存储程序创建的数据&#xff0c;就需要将其保存在磁盘文件中(就是保存在电脑的C盘或D盘中&#xff09;&#xff0c;而只有数据存储起来才可以在其他程序中使用它们。Java的I/O技术可…

机器学习:可解释学习

文章目录 可解释学习为什么需要可解释机器学习可解释还是强模型可解释学习的目标可解释机器学习Local ExplanationGlobal Explanation 可解释学习 神马汉斯&#xff0c;只有在有人看的时候能够答对。 为什么需要可解释机器学习 贷款&#xff0c;医疗需要给出理由&#xff0c;让…

MongoDB 会丢数据吗? 在次补刀MongoDB 双机热备

开头还是介绍一下群&#xff0c;如果感兴趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis &#xff0c;Oracle ,Oceanbase 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请加微信号 liuaustin3 &#xff08;…

makefile开发应用程序的一个通用模板

下面是一个通用的 Makefile 模板&#xff0c;用于开发 C 语言应用程序&#xff1a; # 编译器设置 CC gcc CFLAGS -Wall -Wextra -stdc99# 可执行文件名 TARGET your_program# 源文件和对象文件 SRCS main.c file1.c file2.c OBJS $(SRCS:.c.o)# 默认目标 all: $(TARGET)#…

【C++】异常处理详解

本篇文章重点将会对C中的异常的相关处理操作进行详解。希望本篇文章的内容会对你有所帮助。 目录 一、C语言的异常处理 二、C异常 2、1 异常概念 2、2 异常的使用 2、3 异常类 2、4 异常的重新抛出 三、异常的安全与规范 3、1 异常的安全 3、2 异常的规范 四、异常的优缺点 &am…

CVE-2023-25157:GeoServer OGC Filter SQL注入漏洞复现

CVE-2023-25157&#xff1a;GeoServer OGC Filter SQL注入漏洞复现 前言 本次测试仅供学习使用&#xff0c;如若非法他用&#xff0c;与本文作者无关&#xff0c;需自行负责&#xff01;&#xff01;&#xff01; 一.GeoServer简介 GeoServer 是用 Java 编写的开源软件服务…

界面控件Telerik UI for WPF——Windows 11主题精简模式提升应用体验

Telerik UI for WPF拥有超过100个控件来创建美观、高性能的桌面应用程序&#xff0c;同时还能快速构建企业级办公WPF应用程序。Telerik UI for WPF支持MVVM、触摸等&#xff0c;创建的应用程序可靠且结构良好&#xff0c;非常容易维护&#xff0c;其直观的API将无缝地集成Visua…

【力扣】416. 分割等和子集 <动态规划、回溯>

【力扣】416. 分割等和子集 给你一个 只包含正整数的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 示例 1&#xff1a; 输入&#xff1a;nums [1,5,11,5] 输出&#xff1a;true 解释&#xff1a;数组可以分割成 [1, 5,…

供热管网安全运行监测,提升供热管网安全性能

城市管网是城市的“生命线”之一&#xff0c;是城市赖以生存和发展的基础&#xff0c;在城市基础设施高质量发展中发挥着重要作用。供热管网作为城市生命线中连接供热管线与热用户的桥梁&#xff0c;担负着向企业和居民用户直接供热的重要职责。随着城市热力需求的急剧增加&…

组相联cache如何快速实现cache line eviction并使用PMU events验证

如何快速实现cache line eviction 一&#xff0c;什么是cache hit、miss、linefill、evict &#xff1f;1.1 如果要程序员分别制造出cache hit、miss、linefill、evict这四种场景&#xff0c;该怎么做&#xff1f; 二&#xff0c;实现cache line eviction的方法1.1 直接填充法3…

Android——基本控件(下)(二十)

1. 树型组件&#xff1a;ExpandableListView 1.1 知识点 &#xff08;1&#xff09;掌握树型组件的定义&#xff1b; &#xff08;2&#xff09;可以使用事件对树操作进行监听。 2. 具体内容 既然这个组件可以完成列表的功能&#xff0c;肯定就需要一个可以操作的数据&…

vue从零开始学习

npm install慢解决方法:删掉nodel_modules。 5.0.3:表示安装指定的5.0.3版本 ~5.0.3:表示安装5.0X中最新的版本 ^5.0.3: 表示安装5.x.x中最新的版本。 yarn的优点: 1.速度快,可以并行安装 2.安装版本统一 项目搭建: 安装nodejs查看node版本:node -v安装vue clie : np…

swaggo的一点小理解

如有错误&#xff0c;希望指出&#xff0c;谢谢&#xff01; 很低级的概念不清&#xff0c;大佬嘴下留情。 1.关于swag的注释 我的理解是这些注释是专门提供给Swagger UI界面测试使用的&#xff0c;根据注释内容告诉swag文档这个函数应该有哪些参数&#xff0c;从什么路由走&…

HarmonyOS/OpenHarmony(Stage模型)应用开发单一手势(二)

三、拖动手势&#xff08;PanGesture&#xff09; .PanGestureOptions(value?:{ fingers?:number; direction?:PanDirection; distance?:number}) 拖动手势用于触发拖动手势事件&#xff0c;滑动达到最小滑动距离&#xff08;默认值为5vp&#xff09;时拖动手势识别成功&am…

关于已经安装了TorchCRF,导入时却提示“ModuleNotFoundError: No module named ‘torchcrf‘”的解决办法

应用python时&#xff0c;想导入torchcrf库 from torchcrf import CRF 但系统提示&#xff1a;ModuleNotFoundError: No module named torchcrf 在命令提示符里输入“pip list”检查已安装库&#xff0c;发现torchcrf已经安装 搞了半天&#xff0c;发现是大小写的问题&#x…

Locked勒索病毒:最新变种.locked袭击了您的计算机?

导言&#xff1a; 在今天的数字时代&#xff0c;勒索病毒已经不再是仅仅让数据变得不可访问的小威胁。 .locked 勒索病毒&#xff0c;作为其中的一种&#xff0c;以其高度复杂的加密算法和迅速变化的攻击手法而备受恶意分子喜爱。本文91数据恢复将带您深入了解 .locked 勒索病毒…

C语言练习8(巩固提升)

C语言练习8 编程题 前言 奋斗是曲折的&#xff0c;“为有牺牲多壮志&#xff0c;敢教日月换新天”&#xff0c;要奋斗就会有牺牲&#xff0c;我们要始终发扬大无畏精神和无私奉献精神。奋斗者是精神最为富足的人&#xff0c;也是最懂得幸福、最享受幸福的人。正如马克思所讲&am…

Scala入门,idea关联Scala

Scala 介绍 Scala是一种多规范的编程语言&#xff0c;它结合了面向对象编程&#xff08;OOP&#xff09;和函数式编程&#xff08;FP&#xff09;的特征&#xff0c;Scala的名字源于”Scalable language“&#xff0c;意为”可伸缩语言“。2003年开发的&#xff0c;并在JVM&a…

如何设计一个好的游戏剧情(Part 1:主题的设定)

提醒&#xff1a;此教程仅仅为作者的一些经验和感悟&#xff0c;非专业教程&#xff0c;若介意请前往网上搜集或者书本查阅相关资料&#xff01; 前言&#xff1a;游戏为什么要有剧情——游戏剧情的重要性 游戏剧情的重要性难以低估。一个精彩的剧情可以让玩家感受到强烈的情感…