• 什么是ABA问题


    1、ABA问题描述

    在多线程场景下CAS会出现ABA问题,关于ABA问题这里简单科普下,例如有2个线程同时对同一个值(初始值为A)进行CAS操作,这三个线程如下:

    线程1,期望值为A,欲更新的值为B
    线程2,期望值为A,欲更新的值为B
    

    线程1抢先获得CPU时间片,而线程2因为其他原因阻塞了,线程1取值与期望的A值比较,发现相等然后将值更新为B,然后这个时候出现了线程3,期望值为B,欲更新的值为A,线程3取值与期望的值B比较,发现相等则将值更新为A,此时线程2从阻塞中恢复,并且获得了CPU时间片,这时候线程2取值与期望的值A比较,发现相等则将值更新为B,虽然线程2也完成了操作,但是线程2并不知道值已经经过了A->B->A的变化过程。

    2、解决方法

    要解决ABA问题,可以增加一个版本号,当内存位置V的值每次被修改后,版本号都加1。

    2.1、通过AtomicStampedReference来解决ABA问题

        1)AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时,需要传入初始值和初始版本号;
        2)当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功。

    private static AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(100,1);
    
    public static void main(String[] args) {
        // 第一个线程
        new Thread(() -> {
            int stamp = asr.getStamp();
            System.out.println("t1线程拿到的初始版本号:" + stamp);
            
            // 睡眠1秒,是为了让t2线程也拿到同样的初始版本号
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            
            System.out.println("t1线程第一次compareAndSet结果:" + asr.compareAndSet(100,101,stamp,stamp+1));
            System.out.println("t1线程第二次compareAndSet结果:" + asr.compareAndSet(101,100,stamp+1,stamp+2));
        },"t1").start();
        
        // 第二个线程
        new Thread(() -> {
            int stamp = asr.getStamp(); // 线程t2第一次获取版本号
            System.out.println("t2线程拿到的初始版本号:" + stamp);
            
            // 睡眠3秒,是为了让t1线程完成ABA操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
            }
            
            System.out.println("最新版本号:" + asr.getStamp()); // 线程t2重新获取版本号,看版本号是否变了
            // 下面compareAndSet()第三个参数仍传第一次获取的版本号,如果版本号变了,则更新失败
            System.out.println("t2线程compareAndSet结果:" + asr.compareAndSet(100,200,stamp,asr.getStamp()+1) 
                    + ", 当前值:" + asr.getReference());
        },"t2").start();
    }

      分析

    1、初始值100,初始版本号1
    2、线程t1和t2拿到一样的初始版本号
    3、线程t1完成ABA操作,版本号递增到3
    4、线程t2完成CAS操作,最新版本号已经变成3,跟线程t2之前拿到的版本号1不相等,操作失败

      执行结果

    t1线程拿到的初始版本号:1
    t2线程拿到的初始版本号:1
    t1线程第一次compareAndSet结果:true
    t1线程第二次compareAndSet结果:true
    最新版本号:3
    t2线程compareAndSet结果:false, 当前值:100

    2.2、通过AtomicMarkableReference解决ABA问题

    AtomicStampedReference可以给引用加上版本号,追踪引用的整个变化过程,如:A -> B -> C -> D -> A,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了3次。但是,有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference,AtomicMarkableReference的唯一区别就是不再用int标识引用,而是使用boolean变量——表示引用变量是否被更改过。

    private static AtomicMarkableReference<Integer> amr = new AtomicMarkableReference<>(100,false);
    
    public static void main(String[] args) {
        // 第一个线程
        new Thread(() -> {
            boolean isMarked = amr.isMarked();
            System.out.println("t1线程版本号是否被更改:" + isMarked);
            
            // 睡眠1秒,是为了让t2线程也拿到同样的初始版本号
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            
            System.out.println("t1线程第一次compareAndSet结果:" + amr.compareAndSet(100,101,isMarked,true));
            System.out.println("t1线程第二次compareAndSet结果:" + amr.compareAndSet(101,100,amr.isMarked(),true));
        },"t1").start();
        
        // 第二个线程
        new Thread(() -> {
            boolean isMarked = amr.isMarked();
            System.out.println("t2版本号是否被更改:" + isMarked);
            
            // 睡眠3秒,是为了让t1线程完成ABA操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
            }
            System.out.println("是否更改过:" + amr.isMarked());
            System.out.println("t2线程compareAndSet结果:" + amr.compareAndSet(100,200,isMarked,true) 
                    + ", 当前值:" + amr.getReference());
        },"t2").start();
    }

      执行结果

    t1线程版本号是否被更改:false
    t2版本号是否被更改:false
    t1线程第一次compareAndSet结果:true
    t1线程第二次compareAndSet结果:true
    是否更改过:true
    t2线程compareAndSet结果:false, 当前值:100
  • 相关阅读:
    练习二十七:递归函数应用
    mysql8.0数据库忘记密码时进行修改方法
    格式化字符串两种方式
    练习二十六:阶乘计算(递归)
    练习二十五:阶乘之和计算
    Dapper批量添加
    c# FTP操作类(转)
    c# 依赖注入之---反射(转)
    c# 依赖注入之---setterInjection(转)
    php遍历数组赋值
  • 原文地址:https://www.cnblogs.com/xy-ouyang/p/15238282.html
Copyright © 2020-2023  润新知