Day840.原子类-Java 并发编程实战

news2024/11/25 11:05:06

原子类

Hi,我是阿昌,今天学习记录的是关于原子类

一个累加器的例子,示例代码如下:

在这个例子中,add10K() 这个方法不是线程安全的,问题就出在变量 count 的可见性和 count+=1 的原子性上。

可见性问题可以用 volatile 来解决,而原子性问题前面一直都是采用的互斥锁方案。


public class Test {
  long count = 0;
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 1;
    }
  }
}

其实对于简单的原子性问题,还有一种无锁方案

Java SDK 并发包将这种无锁方案封装提炼之后,实现了一系列的原子类

先看看如何利用原子类解决累加器问题,在下面的代码中,将原来的 long 型变量 count 替换为了原子类 AtomicLong,原来的 count +=1 替换成了 count.getAndIncrement(),仅需要这两处简单的改动就能使 add10K() 方法变成线程安全的,原子类的使用还是挺简单的。


public class Test {
  AtomicLong count = new AtomicLong(0);
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count.getAndIncrement();
    }
  }
}

无锁方案相对互斥锁方案,最大的好处就是性能

互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;

同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。

相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。

那它是如何做到的呢?


一、无锁方案的实现原理

其实原子类性能高的秘密很简单,硬件支持而已。

CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)。

CAS 指令包含 3 个参数:

  • 共享变量的内存地址 A
  • 用于比较的值 B 和共享变量的新值 C
  • 并且只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。

作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。

可以通过下面 CAS 指令的模拟代码来理解 CAS 的工作原理。

在下面的模拟程序中有两个参数,一个是期望值 expect,另一个是需要写入的新值 newValue,只有当目前 count 的值和期望值 expect 相等时,才会将 count 更新为 newValue。


class SimulatedCAS{
  int count;
  synchronized int cas(
    int expect, int newValue){
    // 读目前count的值
    int curValue = count;
    // 比较目前count值是否==期望值
    if(curValue == expect){
      // 如果是,则更新count的值
      count = newValue;
    }
    // 返回写入前的值
    return curValue;
  }
}

“只有当目前 count 的值和期望值 expect 相等时,才会将 count 更新为 newValue。”
要怎么理解这句话呢?

对于前面提到的累加器的例子,count += 1 的一个核心问题是:基于内存中 count 的当前值 A 计算出来的 count+=1 为 A+1,在将 A+1 写入内存的时候,很可能此时内存中 count 已经被其他线程更新过了,这样就会导致错误地覆盖其他线程写入的值(如果你觉得理解起来还有困难。

也就是说,只有当内存中 count 的值等于期望值 A 时,才能将内存中 count 的值更新为计算结果 A+1,这不就是 CAS 的语义吗!

使用 CAS 来解决并发问题,一般都会伴随着自旋,而所谓自旋,其实就是循环尝试。

例如,实现一个线程安全的count += 1操作,“CAS+ 自旋”的实现方案如下所示,首先计算 newValue = count+1,如果 cas(count,newValue) 返回的值不等于 count,则意味着线程在执行完代码①处之后,执行代码②处之前,count 的值被其他线程更新过。那此时该怎么处理呢?

可以采用自旋方案,就像下面代码中展示的,可以重新读 count 最新的值来计算 newValue 并尝试再次更新,直到成功。


class SimulatedCAS{
  volatile int count;
  // 实现count+=1
  addOne(){
    do {
      newValue = count+1; //①
    }while(count !=
      cas(count,newValue) //②
  }
  // 模拟实现CAS,仅用来帮助理解
  synchronized int cas(
    int expect, int newValue){
    // 读目前count的值
    int curValue = count;
    // 比较目前count值是否==期望值
    if(curValue == expect){
      // 如果是,则更新count的值
      count= newValue;
    }
    // 返回写入前的值
    return curValue;
  }
}

通过上面的示例代码,想必你已经发现了,CAS 这种无锁方案,完全没有加锁、解锁操作,即便两个线程完全同时执行 addOne() 方法,也不会有线程被阻塞,所以相对于互斥锁方案来说,性能好了很多。

但是在 CAS 方案中,有一个问题可能会常被你忽略,那就是 ABA 的问题。什么是 ABA 问题呢?

前面提到“如果 cas(count,newValue) 返回的值不等于count,意味着线程在执行完代码①处之后,执行代码②处之前,count 的值被其他线程更新过”,那如果 cas(count,newValue) 返回的值等于count,是否就能够认为 count 的值没有被其他线程更新过呢?

显然不是的,假设 count 原本是 A,线程 T1 在执行完代码①处之后,执行代码②处之前,有可能 count 被线程 T2 更新成了 B,之后又被 T3 更新回了 A,这样线程 T1 虽然看到的一直是 A,但是其实已经被其他线程更新过了,这就是 ABA 问题

可能大多数情况下并不关心 ABA 问题,例如数值的原子递增,但也不能所有情况下都不关心,例如原子化的更新对象很可能就需要关心 ABA 问题,因为两个 A 虽然相等,但是第二个 A 的属性可能已经发生变化了。

所以在使用 CAS 方案的时候,一定要先 check 一下。


二、看 Java 如何实现原子化的 count += 1

在本文开始部分,使用原子类 AtomicLong 的 getAndIncrement() 方法替代了count += 1,从而实现了线程安全。

原子类 AtomicLong 的 getAndIncrement() 方法内部就是基于 CAS 实现的,下面我们来看看 Java 是如何使用 CAS 来实现原子化的count += 1的。

Java 1.8 版本中,getAndIncrement() 方法会转调 unsafe.getAndAddLong() 方法。

这里 this 和 valueOffset 两个参数可以唯一确定共享变量的内存地址。


final long getAndIncrement() {
  return unsafe.getAndAddLong(
    this, valueOffset, 1L);
}

unsafe.getAndAddLong() 方法的源码如下,该方法首先会在内存中读取共享变量的值,之后循环调用 compareAndSwapLong() 方法来尝试设置共享变量的值,直到成功为止。

compareAndSwapLong() 是一个 native 方法,只有当内存中共享变量的值等于 expected 时,才会将共享变量的值更新为 x,并且返回 true;否则返回 fasle。

compareAndSwapLong 的语义和 CAS 指令的语义的差别仅仅是返回值不同而已。


public final long getAndAddLong(
  Object o, long offset, long delta){
  long v;
  do {
    // 读取内存中的值
    v = getLongVolatile(o, offset);
  } while (!compareAndSwapLong(
      o, offset, v, v + delta));
  return v;
}
//原子性地将变量更新为x
//条件是内存中的值等于expected
//更新成功则返回true
native boolean compareAndSwapLong(
  Object o, long offset, 
  long expected,
  long x);

另外,需要注意的是,getAndAddLong() 方法的实现,基本上就是 CAS 使用的经典范例。

所以请你再次体会下面这段抽象后的代码片段,它在很多无锁程序中经常出现。

Java 提供的原子类里面 CAS 一般被实现为 compareAndSet(),compareAndSet() 的语义和 CAS 指令的语义的差别仅仅是返回值不同而已,compareAndSet() 里面如果更新成功,则会返回 true,否则返回 false。


do {
  // 获取当前值
  oldV = xxxx;
  // 根据当前值计算新值
  newV = ...oldV...
}while(!compareAndSet(oldV,newV);

三、原子类概览

Java SDK 并发包里提供的原子类内容很丰富,可以将它们分为五个类别:

  • 原子化的基本数据类型
  • 原子化的对象引用类型
  • 原子化数组
  • 原子化对象属性更新器
  • 原子化的累加器

这五个类别提供的方法基本上是相似的,并且每个类别都有若干原子类,可以通过下面的原子类组成概览图来获得一个全局的印象。

下面详细解读这五个类别。

在这里插入图片描述

1、原子化的基本数据类型

相关实现有 AtomicBoolean、AtomicInteger 和 AtomicLong,提供的方法主要有以下这些,详情可以参考 SDK 的源代码,都很简单,这里就不详细介绍了。


getAndIncrement() //原子化i++
getAndDecrement() //原子化的i--
incrementAndGet() //原子化的++i
decrementAndGet() //原子化的--i
//当前值+=delta,返回+=前的值
getAndAdd(delta) 
//当前值+=delta,返回+=后的值
addAndGet(delta)
//CAS操作,返回是否成功
compareAndSet(expect, update)
//以下四个方法
//新值可以通过传入func函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)

2、原子化的对象引用类型

相关实现有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。

AtomicReference 提供的方法和原子化的基本数据类型差不多,这里不再赘述。不过需要注意的是,对象引用的更新需要重点关注 ABA 问题,AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。

解决 ABA 问题的思路其实很简单,增加一个版本号维度就可以了,这个和乐观锁机制很类似,每次执行 CAS 操作,附加再更新一个版本号,只要保证版本号是递增的,那么即便 A 变成 B 之后再变回 A,版本号也不会变回来(版本号递增的)。AtomicStampedReference 实现的 CAS 方法就增加了版本号参数,方法签名如下:


boolean compareAndSet(
  V expectedReference,
  V newReference,
  int expectedStamp,
  int newStamp) 

AtomicMarkableReference 的实现机制则更简单,将版本号简化成了一个 Boolean 值,方法签名如下:


boolean compareAndSet(
  V expectedReference,
  V newReference,
  boolean expectedMark,
  boolean newMark)

3、原子化数组

相关实现有 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,利用这些原子类,我们可以原子化地更新数组里面的每一个元素。

这些类提供的方法和原子化的基本数据类型的区别仅仅是:

每个方法多了一个数组的索引参数,所以这里也不再赘述了。

4、原子化对象属性更新器

相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的,创建更新器的方法如下:


public static <U>
AtomicXXXFieldUpdater<U> 
newUpdater(Class<U> tclass, 
  String fieldName)

需要注意的是,对象属性必须是 volatile 类型的,只有这样才能保证可见性;

如果对象属性不是 volatile 类型的,newUpdater() 方法会抛出 IllegalArgumentException 这个运行时异常。

会发现 newUpdater() 的方法参数只有类的信息,没有对象的引用,而更新对象的属性,一定需要对象的引用,那这个参数是在哪里传入的呢?

是在原子操作的方法参数中传入的。例如 compareAndSet() 这个原子操作,相比原子化的基本数据类型多了一个对象引用 obj。

原子化对象属性更新器相关的方法,相比原子化的基本数据类型仅仅是多了对象引用参数,所以这里也不再赘述了。


boolean compareAndSet(
  T obj, 
  int expect, 
  int update)

5、原子化的累加器

DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。

如果仅仅需要累加操作,使用原子化的累加器性能会更好。


四、总结

无锁方案相对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题(但可能出现饥饿和活锁问题,因为自旋会反复重试)。

Java 提供的原子类大部分都实现了 compareAndSet() 方法,基于 compareAndSet() 方法,你可以构建自己的无锁数据结构,但是建议你不要这样做,这个工作最好还是让大师们去完成,原因是无锁算法没你想象的那么简单。

Java 提供的原子类能够解决一些简单的原子性问题,但你可能会发现,上面我们所有原子类的方法都是针对一个共享变量的,如果你需要解决多个变量的原子性问题,建议还是使用互斥锁方案。

原子类虽好,但使用要慎之又慎。


下面的示例代码是合理库存的原子化实现,仅实现了设置库存上限 setUpper() 方法,setUpper() 方法的实现是否正确呢?

public class SafeWM {
  class WMRange{
    final int upper;
    final int lower;
    WMRange(int upper,int lower){ //省略构造函数实现 }
  }
  final AtomicReference<WMRange> rf = new AtomicReference<>( new WMRange(0,0) );
  // 设置库存上限
  void setUpper(int v){
    WMRange nr;
    WMRange or = rf.get();
    do{
      // 检查参数合法性
      if(v < or.lower){
        throw new IllegalArgumentException();
      }
      nr = new WMRange(v, or.lower);
    }while(!rf.compareAndSet(or, nr));
  }
}

如果线程1 运行到WMRange or = rf.get();停止,切换到线程2 更新了值,切换回到线程1,进入循环将永远比较失败死循环,解决方案是将读取的那一句放入循环里,CAS每次自旋必须要重新检查新的值才有意义


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

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

相关文章

Java7的异常处理新特性addSuppressed()方法

学习使用Java7新语法try-with-resources&#xff0c;在查看编译文件时&#xff0c;接触到addSuppressed()方法。记录一下使用方式。 先来看一段代码&#xff1a; private static void testt() {try (InputStream is CatchTest.class.getClassLoader().getResourceAsStream(&…

ThinkPHP 多应用模式初探

还是很久以前用tp3.0开发过项目&#xff0c;之后就再没使用过&#xff0c;现在tp都更新到6了&#xff0c;与之前差距很大&#xff0c;需要重新练习掌握最新的tp框架使用及特性。 目录 1.安装框架 2.安装多应用模式扩展think-multi-app 3.目录结构修改并创建应用子目录 4.应…

年后市场将反弹?服装人做好这些准备,才能赚到2023年第一桶金!

目前&#xff0c;随着防疫政策精准落地、逐步放开&#xff0c;人们对疫情的科学认知不断更新&#xff0c;市场活跃度正逐步恢复。秦丝通过与数万服装老板沟通交流&#xff0c;发现新的模式也在渐渐兴起&#xff0c;国内服装市场将有望迎来反弹。 1、消费氛围活跃&#xff0c;市…

善网ESG周报(第六期)

ESG报告&#xff1a; 宁夏建投城运首份社会责任&#xff08;ESG&#xff09;报告正式发布 12月20日&#xff0c;宁夏建投城市运营管理有限公司发布首份ESG报告。报告显示&#xff0c;其公司将业务与环境保护、社会责任、公司治理相结合打造一条绿色发展道路。 国寿股权投资发…

滚动条基本样式设置

::-webkit-scrollbar 系列属性 详细使用说明 ::-webkit-scrollbar注意&#xff1a;如果没有设置滚动溢出的相关属性&#xff0c;滚动条样式系列属性不会生效&#xff08;resize 除外&#xff09;。属性 ::-webkit-scrollbar 整个滚动条。::-webkit-scrollbar-button 滚动条上的…

Vue3组件化开发(一)

文章目录p11 组件组件的拆分和嵌套组件的CSS作用域组件的通信父子组件的通信父组件传递给子组件props的对象用法非prop的attribute子组件传递给父组件案例p11 组件 组件的拆分和嵌套 推荐安装的VS Cdoe插件 组件的CSS作用域 组件的通信 父子组件的通信 父组件传递给子组件…

模型初始化

在深度学习模型训练中&#xff0c;权重初始值极为重要&#xff0c;一个好的初始值会使得模型收敛速度提高&#xff0c;使模型准确率更准确&#xff0c;一般情况下&#xff0c;我们不使用全零初始值训练网络&#xff0c;为了利于训练和减少收敛时间&#xff0c;我们需要对模型进…

从入门到项目实战 - Vue 计算属性用法解析

Vue 计算属性用法解析上一节&#xff1a;《Vue 监听器用法解析 》| 下一节&#xff1a;《Vue 样式绑定》jcLee95 邮箱 &#xff1a;291148484163.com CSDN 主页&#xff1a;https://blog.csdn.net/qq_28550263?spm1001.2101.3001.5343 本文地址&#xff1a;https://blog.…

衣服、商品、商城网站模板首页,仿U袋网,vue+elementui简洁实现(二)

一.前言 接上一遍博客&#xff1a;《衣服、商品、商城网站模板首页&#xff0c;仿U袋网&#xff0c;vueelementui简洁实现》 在此基础上增加了和完善一些页面&#xff1a; 商品分类筛选页面登录、注册、找回密码共用页面U袋学堂&#xff08;视频专区&#xff0c;视频播放&am…

编译原理——参数传递—传名、传地址、得结果、传值

1.传名&#xff08;替换操作&#xff09; 把这种方式理解为替换操作&#xff0c;把P函数参数X、Y、Z和P函数内部的Y、Z替换为A、B&#xff0c;然后P函数对Y、Z的操作&#xff0c;其实就是对A、B的操作&#xff1b;需要注意这和传地址一样&#xff0c;上面对A造成的变化&#x…

制品仓库 Nexus 安装、配置、备份、使用

目录 1.1 Nexus 优点 1.2 Nexus 仓库类型 2. 安装 Nexus 2.1 设置持久化目录 2.2 拉取 Nexus docker 镜像 2.3 运行并启动 Nexus 3. 系统配置 3.1 配置管理员密码 3.2 配置 LDAP 3.3 配置 Email 服务器 4. 配置 Repository 4.1 添加 Blob Stores 4.2 添加 Reposit…

软考高级考哪个好?

软考高级一共5个科目&#xff0c;含金量都差不多&#xff0c;每个人考证的需求各不相同&#xff0c;合适自己情况的才是最有用的证书。看你自己的工作、专业与哪个更相近&#xff0c;再来深入学习备考的&#xff0c;当然自己也要对考试取证有一定的信心。 高级科目介绍&#x…

【LeetCode每日一题】——剑指 Offer II 072.求平方根

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【题目注意】九【时间频度】十【代码实现】十一【提交结果】一【题目类别】 二分查找 二【题目难度】 简单 三【题目编号】 剑指 Offer II 072.求平方根 …

《图解TCP/IP》阅读笔记(第七章 7.5)—— OSPF 开放最短路径优先协议

7.5 OSPF OSPF&#xff08;Open Shortest Path First&#xff0c;开放最短路径优先&#xff09;是一种链路状态性的路由协议&#xff0c;即使网络中有环路&#xff0c;也可以进行稳定的路由控制。 另外&#xff0c;OSPF支持子网掩码&#xff0c;使得在RIP中无法实现的可变长度…

在简历上写了“精通自动化测试,阿里面试官跟我死磕后就给我发了高薪 offer

事情是这样的 前段时间面试了阿里&#xff0c;大家也都清楚&#xff0c;如果你在简历上面写着你精通 XX 技术&#xff0c;那面试官就会跟你死磕到底。 我就是在自己的简历上写了精通自动化测试&#xff0c;然后就开启了和阿里面试官的死磕之路&#xff0c;结果就是拿到了一份…

【Lilishop商城】No4-2.业务逻辑的代码开发,涉及到:会员B端第三方登录的开发-平台注册会员接口开发

仅涉及后端&#xff0c;全部目录看顶部专栏&#xff0c;代码、文档、接口路径在&#xff1a; 【Lilishop商城】记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客 全篇会结合业务介绍重点设计逻辑&#xff0c;其中重点包括接口类、业务类&#xff0c;具体的结合源代…

AMQP协议:消费者、生产者与RibbitMQ节点之间的交互流程,RibbitMQ的核心组成部分

原文链接 一、什么是AMQP协议&#xff1f; AMQP全称&#xff1a;Advanced Message Queuing Protocol(高级消息队列协议)。是应用层协议的一个开发标准&#xff0c;为面向消息的中间件设计。 基于此协议的客户端与消息中间件可以传递消息&#xff0c;不受客户端/中间件的不同产品…

小程序之首页搭建——Flex布局

目录 Flex布局简介 什么是flex布局&#xff1f; flex属性 学习地址&#xff1a; 视图层 View WXML 数据绑定 列表渲染 条件渲染 模板 WXSS 尺寸单位 样式导入 内联样式 选择器 全局样式与局部样式 WXS 示例 注意事项 页面渲染 数据处理 会议OA项目-首页 …

Python实现照片、视频一键压缩及备份源代码

代码 完整代码下载地址&#xff1a;Python实现照片、视频一键压缩及备份源代码 第一次运行前先编辑脚本&#xff0c;修改其中的主库位置、随库位置&#xff0c;保存。 此后要更新随库时&#xff0c;只要双击运行脚本即可。 运行结果示例&#xff1a; 主库位置&#xff1a;D…

用了这么多年的 SpringBoot 你知道什么是 SpringBoot 的 Web 类型推断吗?

用了这么多年的 SpringBoot 那么你知道什么是 SpringBoot 的 web 类型推断吗&#xff1f; 估计很多小伙伴都不知道&#xff0c;毕竟平时开发做项目的时候做的都是普通的 web 项目并不需要什么特别的了解&#xff0c;不过抱着学习的心态&#xff0c;阿粉今天带大家看一下什么是 …