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

news2024/11/28 6:37:36

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

什么是原子性?

事务的一大特性就是原子性(事务具有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): 初始化一个指定值的原子型Integer
    public 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.coloradmin.cn/o/968342.html

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

相关文章

Java8新特性Lambda表达式详细

Comparator&#xff1a;此接口中只包含一个方法 int compare(T A,T B) 如果A>B&#xff0c;返回正数 如果AB&#xff0c;返回0 入伙A<B&#xff0c;返回负数Lambda表达式 函数式重点&#xff1a;只需要关注参数列表和方法体 看参数ctrlp 需求分析 我们在创建线程并启动…

Python常用IDE选择与安装

1、IDE简介 选择一款高效而又顺手的IDE学习或使用Python&#xff0c;可以让你的开发之路充满激情和动力&#xff0c;让你真正投入其中。 常见的Python的IDE工具有&#xff1a; PyCharm 由JetBrains开发的Python IDE&#xff0c;功能强大&#xff0c;支持调试、代码自动完成、…

Java应用CPU占用过高故障排除

一、背景 最近测试反馈测试环境接口偶现有访问超时&#xff0c;然后APP提示是网络失败&#xff0c;看了一下测试环境的应用完全没啥问题&#xff0c;一直以为是网络问题。 今天测试有反馈了&#xff0c;赶紧看了一下测试服务器&#xff0c;这次终于有症状了&#xff0c;CPU直…

【FPGA项目】沙盘演练——基础版报文收发

​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​ 第1个虚拟项目 前言 点灯开启了我们的FPGA之路&#xff0c;那么我们来继续沙盘演练。 用一个虚拟项目&#xff0c;来入门练习&#xff0c;以此步入数字逻辑的…

《C++ primer plus》精炼(OOP部分)——对象和类(1)

聪明在于学习&#xff0c;天才在于积累。所谓天才&#xff0c;实际上是依靠学习。 文章目录 概述正文面向对象编程和面向过程编程类和对象类的组成公共接口 类声明访问控制封装 类和结构体类定义 概述 C是门包罗万象的语言&#xff0c;它将各门类的编程思想杂糅&#xff0c;最…

Parallels Desktop 19新功能解析,助力跨平台工作流程提升!

对于许多Mac用户来说&#xff0c;运行Windows应用程序是必不可少的。也许你的雇主使用的软件只适用于Windows&#xff0c;或者需要使用依赖于某些Windows技术的网站。或者你想在Mac上玩Windows游戏。或者&#xff0c;你可能需要在其他操作系统上测试应用程序和服务——你可以在…

系统vcomp120.dll丢失怎么办?要怎样修复呢?三个修复方法分享

我要和大家分享一个关于系统丢失vcomp120.dll文件的问题。这个问题可能会困扰很多使用电脑的朋友&#xff0c;特别是在运行某些软件时&#xff0c;可能会出现“找不到vcomp120.dll”的错误提示。那么&#xff0c;遇到这样的问题&#xff0c;我们应该如何解决呢&#xff1f;接下…

用 ChatGPT 写代码太省时间了

几个月前&#xff0c;我们聊过陶哲轩使用 ChatGPT 辅助解决数学问题。当时&#xff0c;他觉得虽然测试结果不太令人满意&#xff0c;但也并没有对 ChatGPT 持完全否定的态度。他觉得&#xff0c;像 ChatGPT 这类大型语言模型在数学中可以用来做一些半成品的语义搜索工作&#x…

【leetcode 力扣刷题】汇总区间//合并区间//插入区间

一些关于区间的力扣题目 228. 汇总区间56. 合并区间57. 插入区间 228. 汇总区间 题目链接&#xff1a;228.汇总区间 题目内容&#xff1a; 看题目真是没懂这个题到底是要干啥……实际上题目要求的恰好覆盖数组中所有数字的最小有序区间范围列表&#xff0c;这个最小是指一个区…

QT 设置应用程序图标

1.下载xx.ico图标&#xff1a;ico网址 2.在线PNG转换ICO&#xff1a;png在线转换ico 3.添加图标资源 1&#xff09;新建文件路径 2&#xff09;添加图片资源 3&#xff09;在 .pro文件里面添加图片 4&#xff09;将xx.ico放到工程目录&#xff0c;编译完可以看到xx.exe的图标…

【LeetCode】515.在每个树行中找最大值

题目 给定一棵二叉树的根节点 root &#xff0c;请找出该二叉树中每一层的最大值。 示例1&#xff1a; 输入: root [1,3,2,5,3,null,9] 输出: [1,3,9]示例2&#xff1a; 输入: root [1,2,3] 输出: [1,3]提示&#xff1a; 二叉树的节点个数的范围是 [0,10^4]-2^31 < No…

无涯教程-JavaScript - CUBERANKEDMEMBER函数

描述 CUBERANKEDMEMBER函数返回集合中的第n个或排序的成员。 使用此功能可返回一组中的一个或多个元素,如销售业绩最好的人或前十名的学生。 语法 CUBERANKEDMEMBER (connection, set_expression, rank, [caption])争论 Argument描述Required/OptionalconnectionThe name …

Linux--进程--创建子进程一般目的

父进程创建子进程的目的&#xff1a;简单来说&#xff1a;给特定的输入&#xff0c;给出特定的输出 父进程希望复制自己&#xff0c;使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当请求到达&#xff0c;父进程调用fork&…

FPGA 学习笔记:Vivado 工程管理技巧

前言 当前使用 Xilinx 的 FPGA,所以需要熟悉 Xilinx FPGA 的 开发利器 Vivado 的工程管理方法 这里初步列举一些实际 Xilinx FPGA 开发基于 Vivado 的项目使用到的工程的管理技巧 代码管理 做过嵌入式软件或者其他软件开发的工程技术人员,都会想到使用代码管理工具,如 SVN 、…

架构师成长之路Redis第一篇|Redis 安装介绍以及内存分配器jemalloc

安装 Redis官网:https://redis.io/download/ 下载安装二进制文件 可下载安装最新版Redis7.2.0,或者可选版本6.x 我这里下载6.2.13和7.2最新版本,后面我们都是安装6.2.13版本的信息进行讲解 二进制文件安装步骤 安装前期准备: 安装gcc yum install gcc 压缩文件 tar -xzf re…

SpringCloud(34):Nacos服务发现

1 从单体架构到微服务 1.1单体架构 Web应用程序发展的早期,大部分web工程师将所有的功能模块打包到一起并放在一个web容器中运行,所有功能模块使用同一个数据库,同时,它还提供API或者UI访问的web模块等。 尽管也是模块化逻辑,但是最终它还是会打包并部署为单体式应用,这…

Linux:内核解压缩过程简析

文章目录 1. 前言2. 背景3. zImage 的构建过程4. 内核引导过程5. 内核解压缩过程6. 内核加压缩过程小结7. 参考资料 1. 前言 限于作者能力水平&#xff0c;本文可能存在谬误&#xff0c;因此而给读者带来的损失&#xff0c;作者不做任何承诺。 2. 背景 本文基于 ARM32架构 …

【德哥说库系列】-ASM管理Oracle 19C单实例部署

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

垃圾回收 - 引用计数法

GC原本是一种“释放怎么都无法被引用的对象的机制”。那么人们自然而然就会想到&#xff0c;可以让所有对象事先记录下“有多少程序引用了自己”。让各对象知道自己的“人气指数”&#xff0c;从而让没有人气的对象自己消失&#xff0c;这就是引用计数法。 1、计数器 计数器表…

【Unity基础】1.项目搭建与视图编辑

【Unity基础】1.项目搭建与视图编辑 大家好&#xff0c;我是Lampard~~ 欢迎来到Unity基础系列博客&#xff0c;终于要开始写基础系列的博客了&#xff0c;前两篇的内容基本上与入门系列相同&#xff0c;如果有紧跟入门系列的同学可以直接从第三篇文章开始看 好了话不多说我们开…