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…

图论板子——spfa

bool st[N];//是否在队列中 int d[N];//到起点的距离 void dj(int S,int T)//从S到T {queue<int>q;q.push(S);memset(d,0x3f,sizeof d);d[S] 0;while (!q.empty()){int ver q.front();q.pop();st[ver] false;//出队for (int i h[ver];i ! -1;i h[i]){int x e[i];in…

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

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

图论模板——最大流问题

Dinic算法的时间复杂度为O&#xff08;n^2m&#xff09;。实际运用远远小于这个上界。 特别的&#xff0c;Dinic算法求解二分图最大匹配的时间复杂度为O&#xff08;msqrt&#xff08;n&#xff09;&#xff09; 最大流问题模板 #include <bits/stdc.h> using namespace…

python简述目录_Python源码下载和目录简介(示例代码)

Python源码下载和目录简介一、Python源码下载1、Linux操作系统下使用终端命令下载&#xff1a;wget https://www.python.org/ftp/python/3.7.4/Python-3.7.4.tgz // 获取源码压缩包tar -xf Python-3.7.4.tgz // 解压2、非Linux操作系统下载&#xff1a;(2)滑到最下面&#xff0…

Linux之Ubuntu安装搜狗输入法

1.下载搜狗输入法安装包 搜狗官网&#xff1a;https://pinyin.sogou.com/linux/ 2.更新ubuntu内置的包管理器apt-get的软件源[如果中途安装失败&#xff0c;经常是此原因造成的] sudo apt-get update sudo aot-get upgrade //如果有需要的话 3.dpkg -i &#xff08;下载搜狗下来…

数据结构板子——splay

维护数列 https://www.acwing.com/problem/content/957/ 线段树能写的splay都能写&#xff0c;不过splay常数较大。时间复杂度大概是线段树的三倍 #include<bits/stdc.h> using namespace std; const int N 500010,INF 1e9; int n,m; struct Node {int s[2],p,v;int r…

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

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

23种设计模式之原型模式代码实例

原型模式就是利用一个克隆”原型“来产生新对象的一种模式&#xff0c; 克隆又分浅克隆与深克隆&#xff0c; 浅克隆&#xff1a;将一个对象复制后&#xff0c;基本数据类型的变量都会重新创建&#xff0c;而引用类型&#xff0c;指向的还是原对象所指向的。 深克隆&#xff1a…

图论模板——费用流(无法处理负环)

费用流 https://www.acwing.com/problem/content/2176/ //最大流的最小费用 #include<bits/stdc.h> using namespace std;const int N 5010, M 100010, INF 1e8;int n,m,S,T; int ne[M],e[M],f[M],w[M],h[N],idx; int q[N],d[N],pre[N],incf[N]; bool st[N];void add…

css调用外部字体

CSS中可以使用font-face属性即可实现调用任何外部等特殊字体。 font-face属性介绍及其实例&#xff1a; 对浏览器的支持&#xff1a; Firefox、Chrome、Safari 以及 Opera 支持 .ttf (True Type Fonts) 和 .otf (OpenType Fonts) 类型的字体。 Internet Explorer 9 支持新的 fo…

python注入点查找_sqlmap常用注入点检测爆破命令

1、检测注入点是否可用python sqlmap.py -u "url"2、从目标url爆破所有数据库名python sqlmap.py -u "url" --dbs3、从目标url爆破当前数据库名python sqlmap.py -u "url" --current-dbs4、列出数据库所有用户python sqlmap.py -u "url&quo…

python如何输入空行_在python中,如何在接受用户输入时跳过空行?

您得到的行为是预期的&#xff0c;请阅读input文档。在input([prompt])If the prompt argument is present, it is written to standard output without a trailing newline. The function then reads a line from input, converts it to a string (stripping a trailing newli…

js层级选择框样式_【JavaWeb】85:jQuery的各种选择器

今天是刘小爱自学Java的第85天。感谢你的观看&#xff0c;谢谢你。话不多说&#xff0c;开始今天的学习&#xff1a;选择器的作用是什么&#xff1f;可以快速准确地定位我们所需要的标签。刚学CSS的时候&#xff0c;觉得CSS选择器也太多了吧&#xff0c;直到今天学了jQuery选择…

python的单例模式

单例模式(Singleton Pattern)&#xff0c;是一种软件设计模式&#xff0c;是类只能实例化一个对象&#xff0c; 目的是便于外界的访问&#xff0c;节约系统资源&#xff0c;如果希望系统中 只有一个对象可以访问&#xff0c;就用单例模式&#xff0c; 显然单例模式的要点有三个…

c语言自定义输出小数点位数_C语言中输出时怎样控制小数点后的位数,请举例说明......

控制2113小数位数就是通过输出格式说明符来规定的举例5261说明如下4102&#xff1a;1、float f13.1415926;2、float f21234.1415926;3、float f3124.1;4、printf("%3.4f",f1);//输出结果为&#xff1a;_ _ 3.1416(_表示空格1653)5、printf("%3.4f",f2);//输…

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

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

python在不同层级目录import模块的方法

转自&#xff1a;https://www.cnblogs.com/luoye00/p/5223543.html 使用python进行程序编写时&#xff0c;经常会使用第三方模块包。这种包我们可以通过python setup install 进行安装后&#xff0c;通过import XXX或from XXX import yyy 进行导入。不过如果是自己遍写的依赖包…

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

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

php: +1天, +3个月, strtotime(): +1 day, +3 month

php: 1天, 3个月, strtotime(): 1 day, 3 month 比如&#xff0c;我现在当前时间基础上1天&#xff1a; strtotime("1 day"); 比如我现在&#xff0c;2014-05-01时间上 3个月 $s strtotime("2014-05-01"); $d strtotime("3 month", $s); 转载…