atomic原子类实现机制_并发编程:并发操作原子类Atomic以及CAS的ABA问题

本文基于JDK1.8

Atomic原子类

原子类是具有原子操作特征的类。

原子类存在于java.util.concurrent.atmic包下。

根据操作的数据类型,原子类可以分为以下几类。

基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

AtomicInteger的常用方法

public final int get() //获取当前的值public final int getAndSet(int newValue)//获取当前的值,并设置新的值public final int getAndIncrement()//获取当前的值,并自增public final int getAndDecrement() //获取当前的值,并自减public final int getAndAdd(int delta) //加上给定的值,并返回之前的值public final int addAndGet(int delta) //加上给定的值,并返回最终结果boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicInteger常见方法的使用

@Testpublic void AtomicIntegerT() {    AtomicInteger c = new AtomicInteger();    c.set(10);    System.out.println("初始设置的值 ==>" + c.get());    int andAdd = c.getAndAdd(10);    System.out.println("为原先的值加上10,并返回原先的值,原先的值是 ==> " + andAdd + "加上之后的值是 ==> " + c.get());    int finalVal = c.addAndGet(5);    System.out.println("加上5, 之后的值是 ==> " + finalVal);    int i = c.incrementAndGet();    System.out.println("++1,之后的值为 ==> " + i);        int result = c.updateAndGet(e -> e + 3);    System.out.println("可以使用函数式更新 + 3 计算后的结果为 ==> "+ result);    int res = c.accumulateAndGet(10, (x, y) -> x + y);    System.out.println("使用指定函数计算后的结果为 ==>" + res);}初始设置的值 ==>10为原先的值加上10,并返回原先的值,原先的值是 ==> 10 加上之后的值是 ==> 20加上5, 之后的值是 ==> 25++1,之后的值为 ==> 26可以使用函数式更新 + 3 计算后的结果为 ==> 29使用指定函数计算后的结果为 ==>39

AtomicInteger保证原子性

我们知道,volatile可以保证可见性和有序性,但是不能保证原子性,因此,以下的代码在并发环境下的结果会不正确:最终的结果可能会小于10000。

public class AtomicTest {    static CountDownLatch c = new CountDownLatch(10);    public volatile int inc = 0;    public static void main(String[] args) throws InterruptedException {        final AtomicTest test = new AtomicTest();        for (int i = 0; i < 10; i++) {            new Thread(() -> {                for (int j = 0; j < 1000; j++) {                    test.increase();                }                c.countDown();            }).start();        }        c.await();        System.out.println(test.inc);    }    //不是原子操作, 先读取inc的值, inc + 1, 写回内存    public void increase() {        inc++;    }}

想要解决最终结果不是10000的办法有两个:

  • 使用synchronized关键字,修饰increase方法,锁可以保证该方法某一时刻只能有一个线程执行,保证了原子性。
    public synchronized void increase() {        inc++;    }
  • 使用Atomic原子类,比如这里的AtomicInteger。
public class AtomicTest {    static CountDownLatch c = new CountDownLatch(10);    // 使用整型原子类 保证原子性    public AtomicInteger inc = new AtomicInteger();    public static void main(String[] args) throws InterruptedException {        final AtomicTest test = new AtomicTest();        for (int i = 0; i < 10; i++) {            new Thread(() -> {                for (int j = 0; j < 1000; j++) {                    test.increase();                }                c.countDown();            }).start();        }        c.await();        System.out.println(test.getCount());    }    // 获取当前的值,并自增    public void increase() {        inc.getAndIncrement();    }    // 获取当前的值    public int getCount() {        return inc.get();    }}

getAndIncrement()方法的实现

getAndIncrement方法是如何确保原子操作的呢?

    private static final Unsafe unsafe = Unsafe.getUnsafe();    private static final long valueOffset;    static {        try {            //objectFieldOffset本地方法,用来拿到“原来的值”的内存地址。            valueOffset = unsafe.objectFieldOffset                (AtomicInteger.class.getDeclaredField("value"));        } catch (Exception ex) { throw new Error(ex); }    }//value在内存中可见,JVM可以保证任何时刻任何线程总能拿到该变量的最新值    private volatile int value;   public final int incrementAndGet() {        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;    }

openjdk1.8Unsafe类的源码:Unsafe.java

    /**     * Atomically adds the given value to the current value of a field     * or array element within the given object o     * at the given offset.     *     * @param o object/array to update the field/element in     * @param offset field/element offset     * @param delta the value to add     * @return the previous value     * @since 1.8     */    public final int getAndAddInt(Object o, long offset, int delta) {        int v;        do {            v = getIntVolatile(o, offset);        } while (!compareAndSwapInt(o, offset, v, v + delta));        return v;    }

Java的源码改动是有的,《Java并发编程的艺术》的内容也在此摘录一下,相对来说更好理解一些:

    public final int getAddIncrement() {        for ( ; ; ) {            //先取得存储的值            int current = get();            //加1操作            int next = current + 1;            // CAS保证原子更新操作,如果输入的数值等于预期值,将值设置为输入的值            if (compareAndSet(current, next)) {                return current;            }        }    }    public final boolean compareAndSet(int expect, int update) {        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

数组类型

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray :引用类型数组原子类

AtomicIntegerArray的常用方法

@Testpublic void AtomicIntegerArrayT() {    int[] nums = {1, 2, 3, 4, 5};    AtomicIntegerArray c = new AtomicIntegerArray(nums);    for (int i = 0; i < nums.length; i++) {        System.out.print(c.get(i) + " ");    }    System.out.println();    int finalVal = c.addAndGet(0, 10);    System.out.println("索引为 0 的值 加上 10  ==> " + finalVal);    int i = c.incrementAndGet(0);    System.out.println("索引为 0 的值 ++1,之后的值为 ==> " + i);    int result = c.updateAndGet(0, e -> e + 3);    System.out.println("可以使用函数式更新索引为0 的位置 + 3 计算后的结果为 ==> " + result);    int res = c.accumulateAndGet(0, 10, (x, y) -> x * y);    System.out.println("使用指定函数计算后的结果为 ==> " + res);}

引用类型

基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。

  • AtomicReference:引用类型原子类
  • AtomicMarkableReference:原子更新带有标记的引用类型,无法解决ABA问题,该类的标记更多用于表示引用值是否已逻辑删除
  • AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

AtomicReference常见方法的使用

@Testpublic void AtomicReferenceT(){    AtomicReference ar = new AtomicReference<>();    Person p = new Person(18,"summer");    ar.set(p);    Person pp = new Person(50,"dan");    ar.compareAndSet(p, pp);// except = p  update = pp    System.out.println(ar.get().getName());    System.out.println(ar.get().getAge());}@Data@AllArgsConstructor@NoArgsConstructorclass Person{    int age;    String name;}//dan//50

对象的属性修改类型

如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段

要想原子地更新对象的属性需要两步。

  1. 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
  2. 更新的对象属性必须使用 public volatile 修饰符。

AtomicIntegerFieldUpdater常用方法的使用

@Testpublic void AtomicIntegerFieldUpdateTest(){    AtomicIntegerFieldUpdater a =        AtomicIntegerFieldUpdater.newUpdater(Person.class,"age");    Person p = new Person(18,"summer");    System.out.println(a.getAndIncrement(p)); //18    System.out.println(a.get(p)); //19}@Data@AllArgsConstructor@NoArgsConstructorclass Person{    public volatile int age;    private String name;}

Java8新增的原子操作类

  • LongAdder

由于AtomicLong通过CAS提供非阻塞的原子性操作,性能已经很好,在高并发下大量线程竞争更新同一个原子量,但只有一个线程能够更新成功,这就造成大量的CPU资源浪费。

LongAdder 通过让多个线程去竞争多个Cell资源,来解决,在很高的并发情况下,线程操作的是Cell数组,并不是base,在cell元素不足时进行2倍扩容,在高并发下性能高于AtomicLong

CAS的ABA问题的产生

假设两个线程访问同一变量x。

  1. 第一个线程获取到了变量x的值A,然后执行自己的逻辑。
  2. 这段时间内,第二个线程也取到了变量x的值A,然后将变量x的值改为B,然后执行自己的逻辑,最后又把变量x的值变为A【还原】。
  3. 在这之后,第一个线程终于进行了变量x的操作,但此时变量x的值还是A,因为x的值没有变化,所以compareAndSet还是会成功执行。

先来看一个值变量产生的ABA问题,理解一下ABA问题产生的流程:

@SneakyThrows@Testpublic void test1() {    AtomicInteger atomicInteger = new AtomicInteger(10);    CountDownLatch countDownLatch = new CountDownLatch(2);    new Thread(() -> {        atomicInteger.compareAndSet(10, 11);        atomicInteger.compareAndSet(11,10);        System.out.println(Thread.currentThread().getName() + ":10->11->10");        countDownLatch.countDown();    }).start();    new Thread(() -> {        try {            TimeUnit.SECONDS.sleep(1);            boolean isSuccess = atomicInteger.compareAndSet(10,12);            System.out.println("设置是否成功:" + isSuccess + ",设置的新值:" + atomicInteger.get());        } catch (InterruptedException e) {            e.printStackTrace();        }        countDownLatch.countDown();    }).start();    countDownLatch.await();}//输出:线程2并没有发现初始值已经被修改//Thread-0:10->11->10//设置是否成功:true,设置的新值:12

ABA问题存在,但可能对值变量并不会造成结果上的影响,但是考虑一种特殊的情况:

https://zhuanlan.zhihu.com/p/237611535

b590304c7bc9b89630ef987dc1a70ca3.png
  1. 线程1和线程2并发访问ConcurrentStack。
  2. 线程1执行出栈【预期结果是弹出B,A成为栈顶】,但在读取栈顶B之后,被线程2抢占。
  3. 线程2记录栈顶B,依次弹出B和A,再依次将C,D,B入栈,且保证B就是原栈顶记录的B。
  4. 之后轮到线程1,发现栈顶确实是期望的B,虽弹出B,但此时栈顶已经是D,就出现了错误。

BAB的问题如何解决

AtomicStampedReference 原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

@SneakyThrows@Testpublic void test2() {    AtomicStampedReference atomicStampedReference = new AtomicStampedReference(10,1);    CountDownLatch countDownLatch = new CountDownLatch(2);    new Thread(() -> {        System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp());        atomicStampedReference.compareAndSet(10, 11, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);        System.out.println(Thread.currentThread().getName() + " 第二次版本:" + atomicStampedReference.getStamp());        atomicStampedReference.compareAndSet(11, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);        System.out.println(Thread.currentThread().getName() + " 第三次版本:" + atomicStampedReference.getStamp());        countDownLatch.countDown();    }).start();    new Thread(() -> {        System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp());        try {            TimeUnit.SECONDS.sleep(2);            boolean isSuccess = atomicStampedReference.compareAndSet(10,12, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);            System.out.println(Thread.currentThread().getName() + " 修改是否成功:" + isSuccess + " 当前版本:" + atomicStampedReference.getStamp() + " 当前值:" + atomicStampedReference.getReference());            countDownLatch.countDown();        } catch (InterruptedException e) {            e.printStackTrace();        }    }).start();    countDownLatch.await();}//输出//输出Thread-0 第一次版本:1Thread-0 第二次版本:2Thread-0 第三次版本:3Thread-1 第一次版本:3Thread-1 修改是否成功:true 当前版本:4 当前值:12

而AtomicMarkableReference 通过标志位,标志位只有true和false,每次更新标志位的话,在第三次的时候,又会变得跟第一次一样,并不能解决ABA问题。

@SneakyThrows@Testpublic void test3() {    AtomicMarkableReference markableReference = new AtomicMarkableReference<>(10, false);    CountDownLatch countDownLatch = new CountDownLatch(2);    new Thread(() -> {        System.out.println(Thread.currentThread().getName() + " 第一次标记:" + markableReference.isMarked());        markableReference.compareAndSet(10, 11, markableReference.isMarked(), true);        System.out.println(Thread.currentThread().getName() + " 第二次标记:" + markableReference.isMarked());        markableReference.compareAndSet(11, 10, markableReference.isMarked(), false);        System.out.println(Thread.currentThread().getName() + " 第三次标记:" + markableReference.isMarked());        countDownLatch.countDown();    }).start();    new Thread(() -> {        System.out.println(Thread.currentThread().getName() + " 第一次标记:" + markableReference.isMarked());        try {            TimeUnit.SECONDS.sleep(2);            boolean isSuccess = markableReference.compareAndSet(10,12, false, true);            System.out.println(Thread.currentThread().getName() + " 修改是否成功:" + isSuccess + " 当前标记:" + markableReference.isMarked() + " 当前值:" + markableReference.getReference());            countDownLatch.countDown();        } catch (InterruptedException e) {            e.printStackTrace();        }    }).start();    countDownLatch.await();}//输出Thread-0 第一次标记:falseThread-0 第二次标记:trueThread-0 第三次标记:falseThread-1 第一次标记:falseThread-1 修改是否成功:true 当前标记:true 当前值:12

如果觉得本文对你有帮助,可以转发关注支持一下

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

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

相关文章

c# winform窗体如何设置才可以不能随意拖动大小

执行以下两个步骤&#xff0c;能够禁止用户改变窗体的大小 &#xff08;一&#xff09;步骤1 设置窗体的FormBorderStyle属性为下列五个值中的任意一个 None&#xff1a;将窗口设置为无边框、无标题栏。用户无法改变窗口的大小&#xff0c;也无法改变窗口显示的位置&#xff1b…

增加数据_咱晋城人口又增加了?最新数据来了

微信广告合作/13903568008、13663561666近日山西省统计局山西省人口抽样调查办公室联合发布2019年山西省人口变动情况抽样调查主要数据公报全省哪个地市人最多&#xff1f;男女比例如何&#xff1f;……1常住人口根据抽样调查全省人口出生率为9.12‰比上年下降0.51个千分点人口…

怎么让wegame适应屏幕大小_iOS的五大设计原则:统一化和适应化原则

昨天米醋跟大家分享了iOS的五大设计原则中凸显内容原则&#xff0c;那么今天我们继续来说说统一化原则和适应化原则。统一化原则统一化原则主要体现在视觉统一和交互统一两个方面。在视觉统一方面&#xff0c;要讲究字体、颜色和元素的统一性&#xff0c;标题字号的统一&#x…

语言求圆周率近似值改错_新证明解决了如何求无理数的近似值

原文&#xff1a;Kevin Hartnett&#xff0c;QuantaMagazine日前&#xff0c;一份新鲜出炉的论文证明了近80年悬而未决的达芬-谢弗&#xff08;Duffin-Schaeffer&#xff09;猜想&#xff0c;让数轴上讳莫如深的部分再也不如表面看来那么遥不可及。达芬-谢弗猜想的证明完美解答…

谷歌翻译无法连接网络_window10无法连接网络

很多朋友都将电脑的系统升级到Win10&#xff0c;如果遇到了Win10系统无法连接网络该如何解决&#xff0c;下面就为大家介绍一下解决的方法。window10无法连接网络一、检查路由器1、重新启动(断电10秒钟) 无线路由器和猫(调制解调器)&#xff0c;有时候是路由器的故障&#xff1…

获取弹出的窗口_Win7系统如何获取设置everyone权限的问题

一位win7之家系统的小伙伴&#xff0c;想要在电脑系统中获取everyone权限&#xff0c;但是不知道该怎么做&#xff0c;对于Win7电脑如何获取设置everyone权限这个问题&#xff0c;小编觉得我们可以在电脑的计算机中找到相关的磁盘&#xff0c;打开磁盘属性然后在安全选项中进行…

异步请求中jetty处理ServletRequestListener的坑

标题起得比较诡异&#xff0c;其实并不是坑&#xff0c;而是jetty似乎压根就没做对异步request的ServletRequestListener的特殊处理&#xff0c;如果文中有错误欢迎提出&#xff0c;可能自己有所疏漏了。 之前遇到了一个bug&#xff0c;在Listener中重写requestDestroyed清理资…

华为h22h05服务器做raid_华为V5服务器 RAID控制卡(LSI SAS3008IR)

提供高速接口和模块LSI SAS3008IR的PCIe Core提供PCIe x8接口&#xff0c;每lane速率为8Gb/s&#xff0c;可以兼容x1、x2、x4配置&#xff1b;支持PCIe 3.0规格&#xff0c;兼容PCIe 2.x和PCIe 1.x。LSI SAS3008IR的SAS模块提供SAS功能&#xff0c;并定义支持的硬盘速率。LSI S…

css加了固定定位就不显示内容_前端开发必备,学好”定位“向菜鸟说拜拜

众所周知&#xff0c;前端CSS中&#xff0c;盒模型、浮动、定位为必须掌握的三座大山。今天就来聊聊定位的那些事。定位是什么&#xff1f;先来看看哪些场景用到定位&#xff0c;如下图所示&#xff0c;凡是有盒子压住另一个盒子的地方都可定位&#xff0c;因为用浮动做不了&am…

vscode更换主题的插件_VScode 插件开发(三)主题

咱们上回书说道&#xff0c;安装完基础环境&#xff0c;我们要来玩自己的主题了1. 创建一个主题项目$ yo code选中 New Color Theme接下来照图中所选&#xff0c;完成项目创建(简单英语不做解释)打开项目如图2. 配置文件2.1 themes这个文件夹包含主题配置文件&#xff0c;可以新…

软件工程概论课后作业01

1. 网站系统开发需要掌握的技术 ①java语言 Java语言体系比较庞大&#xff0c;包括多个模块。从WEB项目应用角度讲有JSP&#xff0c;Servlet&#xff0c;JDBC&#xff0c;JavaBean&#xff08;Application&#xff09;四部分技术。JDBC可做三件事情&#xff1a;与数据库建立连接…

mysql 参数bug_MySQL 的这个 BUG,坑了多少人?

作者&#xff1a;腾讯数据库技术来源&#xff1a;cloud.tencent.com/developer/article/1367681▌问题描述近期&#xff0c;线上有个重要Mysql客户的表在从5.6升级到5.7后&#xff0c;master上插入过程中出现"Duplicate key"的错误&#xff0c;而且是在主备及RO实例上…

i9 9900k mysql_i9-9900K和9900KS有什么区别?i9-9900KS和i9-9900K区别对比评测

众所周知&#xff0c;i9-9900KF相当于i9-9900K去除核显的版本&#xff0c;其它参考保持一致&#xff0c;所以在性能上也是相同的。不过在近期&#xff0c;intel牌牙膏厂再一次发布了一款九代酷睿i9-9900KS特别版&#xff0c;从产品型号S后缀上来看&#xff0c;确实有点类似于NV…

Memory及其controller芯片整体测试方案(上篇)

如果你最近想买手机&#xff0c;没准儿你一看价格会被吓到手机什么时候偷偷涨价啦&#xff01; 其实对于手机涨价&#xff0c;手机制造商也是有苦难言&#xff0c;其中一个显著的原因是存储器芯片价格的上涨↗↗↗ >>> 存储器memory的江湖地位 存储器memory&#xff0…

BootStrapJS——modal弹出框

学习参考视频https://ninghao.net/video/1615 1.对话框 - Modal 需要bootstrap的CSS文件引入&#xff0c;以及jQuery的js包 1 <!DOCTYPE html>2 <html lang"en">3 <head>4 <meta charset"UTF-8">5 <title>弹窗测试&…

mysql.ini环境配置_MySQL配置文件mysql.ini参数详解

my.ini(Linux系统下是my.cnf)&#xff0c;当mysql服务器启动时它会读取这个文件&#xff0c;设置相关的运行环境参数。my.ini分为两块&#xff1a;Client Section和Server Section。Client Section用来配置MySQL客户端参数。要查看配置参数可以用下面的命令&#xff1a;show va…

mysql数据备份在哪里_mysql之数据备份与恢复

本文内容&#xff1a; 复制文件法利用mysqldump利用select into outfile其它(列举但不介绍)首发日期&#xff1a;2018-04-19有些时候&#xff0c;在备份之前要先做flush tables &#xff0c;确保所有数据都被写入到磁盘中。复制文件法&#xff1a;对于myisam存储引擎的数据库&a…

python opencv 边缘检测_Python使用Opencv实现边缘检测以及轮廓检测的实现

边缘检测Canny边缘检测器是一种被广泛使用的算法&#xff0c;并被认为是边缘检测最优的算法&#xff0c;该方法使用了比高斯差分算法更复杂的技巧&#xff0c;如多向灰度梯度和滞后阈值化。Canny边缘检测器算法基本步骤&#xff1a;平滑图像&#xff1a;通过使用合适的模糊半径…

web程序入门五(http无状态)

Asp.net中的状态保持方案&#xff1a; ViewState&#xff1a;是donet特有的 微软提供的对象来完成状态保持 ViewState["key1"] 11; 赋值 键值对 形式 object类型 ViewState["key1"] 直接取值&#xff0c;将其转化成相应的类型 原理&#xff1a;本质…

互评Beta版本(Hello World!——SkyHunter)

1 基于NABCD评论作品&#xff0c;及改进建议 SkyHunter这款游戏我很喜欢&#xff0c;小时候总玩飞机类的游戏&#xff0c;这款游戏我上课的时候试玩了&#xff0c;在我电脑上运行是很好玩的&#xff0c;音乐震撼&#xff0c;画面玄幻&#xff0c;富有金属音乐的味道&#xff0c…