java并发编程十 ABA 问题及解决,原子数组和字段更新

ABA 问题及解决

ABA 问题

ABA问题是在使用CAS(Compare and Swap)操作时可能遇到的一种典型问题。 它指的是一个共享变量的值在操作期间从A变为B,然后再从B变回A,而CAS操作可能会错误地认为没有其他线程修改过这个值。 这会导致CAS操作的误判,可能会引发潜在的问题。

static AtomicReference<String> ref = new AtomicReference<>("A");
 public static void main(String[] args) throws InterruptedException {
 log.debug("main start...");
 // 获取值 A
 // 这个共享变量被它线程修改过?
String prev = ref.get();
 other();
 sleep(1);
 // 尝试改为 C
 log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
 }
  private static void other() {
 new Thread(() -> {
 log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
 },"t1").start();
 sleep(0.5);
 new Thread(() -> {
 log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
    }, 
"t2").start();
 }

输出

11:29:52.325 c.Test36 [main] - main start... 
11:29:52.379 c.Test36 [t1] - change A->B true 
11:29:52.879 c.Test36 [t2] - change B->A true 
11:29:53.880 c.Test36 [main] - change A->C true 

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程希望:
只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号 AtomicStampedReference

static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
 public static void main(String[] args) throws InterruptedException {
 log.debug("main start...");
 // 获取值 A
 String prev = ref.getReference();
 // 获取版本号
int stamp = ref.getStamp();
 log.debug("版本 {}", stamp);
 // 如果中间有其它线程干扰,发生了 ABA 现象
other();
 sleep(1);
 // 尝试改为 C
 log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
 }
 private static void other() {
 new Thread(() -> {
 log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", 
ref.getStamp(), ref.getStamp() + 1));
 log.debug("更新版本为 {}", ref.getStamp());
    }, 
"t1").start();
 sleep(0.5);
 new Thread(() -> {
 log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", 
ref.getStamp(), ref.getStamp() + 1));
 log.debug("更新版本为 {}", ref.getStamp());
    }, 
"t2").start();
}

输出为

15:41:34.891 c.Test36 [main] - main start... 
15:41:34.894 c.Test36 [main] - 版本 0 
15:41:34.956 c.Test36 [t1] - change A->B true 
15:41:34.956 c.Test36 [t1] - 更新版本为 1 
15:41:35.457 c.Test36 [t2] - change B->A true 
15:41:35.457 c.Test36 [t2] - 更新版本为 2 
15:41:36.457 c.Test36 [main] - change A->C false 

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A ->C,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。

但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了
AtomicMarkableReference.
在这里插入图片描述

AtomicMarkableReference

class GarbageBag {
 String desc;
 public GarbageBag(String desc) {
 this.desc = desc;
    }
 public void setDesc(String desc) {
 this.desc = desc;
     }
 @Override
 public String toString() {
 return super.toString() + " " + desc;
    }
 }
 @Slf4j
 public class TestABAAtomicMarkableReference {
 public static void main(String[] args) throws InterruptedException {
 GarbageBag bag = new GarbageBag("装满了垃圾");
 // 参数2 mark 可以看作一个标记,表示垃圾袋满了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
 log.debug("主线程 start...");
 GarbageBag prev = ref.getReference();
 log.debug(prev.toString());
 new Thread(() -> {
 log.debug("打扫卫生的线程 start...");
 bag.setDesc("空垃圾袋");
 while (!ref.compareAndSet(bag, bag, true, false)) {}
 log.debug(bag.toString());
        }).start();
 Thread.sleep(1000);
 log.debug("主线程想换一只新垃圾袋?");
 boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
 log.debug("换了么?" + success);
 log.debug(ref.getReference().toString());
 }
 }

输出

2023-10-13 15:30:09.264 [main] 主线程 start... 
2023-10-13 15:30:09.270 [main] cn.onenewcode.GarbageBag@5f0fd5a0 装满了垃圾 
2023-10-13 15:30:09.293 [Thread-1] 打扫卫生的线程 start... 
2023-10-13 15:30:09.294 [Thread-1] cn.onenewcode.GarbageBag@5f0fd5a0 空垃圾袋 
2023-10-13 15:30:10.294 [main] 主线程想换一只新垃圾袋? 
2023-10-13 15:30:10.294 [main] 换了么?false 
2023-10-13 15:30:10.294 [main] cn.onenewcode.GarbageBag@5f0fd5a0 空垃圾袋 

可以注释掉打扫卫生线程代码,再观察输出

原子数组

字段更新器,主要是用来更新自定义类的字段的。Java 提供以下三种字段更新器:

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

有如下方法

/**
参数1,提供数组、可以是线程不安全数组或线程安全数组
参数2,获取数组长度的方法
参数3,自增方法,回传 array, index
参数4,打印数组的方法
*/
 // supplier 提供者 无中生有  ()->结果
// function 函数   一个参数一个结果   (参数)->结果  ,  BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果  (参数)->void,      BiConsumer (参数1,参数2)->
 private static <T> void demo(
 Supplier<T> arraySupplier,
 Function<T, Integer> lengthFun,
 BiConsumer<T, Integer> putConsumer,
 Consumer<T> printConsumer ) {
 List<Thread> ts = new ArrayList<>();
 T array = arraySupplier.get();
 int length = lengthFun.apply(array);
 for (int i = 0; i < length; i++) {
 // 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
 for (int j = 0; j < 10000; j++) {
 putConsumer.accept(array, j%length);
            }
        }));
        }
 ts.forEach(t -> t.start()); // 启动所有线程
ts.forEach(t -> {
 try {
 t.join();
        } 
catch (InterruptedException e) {
 e.printStackTrace();
  }
    }); 
    printConsumer.accept(array);
    }

不安全的数组

demo(
    ()->new int[10],
    (array)->array.length,
    (array, index) -> array[index]++,
 array-> System.out.println(Arrays.toString(array))
 );

结果

[9870, 9862, 9774, 9697, 9683, 9678, 9679, 9668, 9680, 9698] 

安全的数组

demo(
    ()-> new AtomicIntegerArray(10),
    (array) -> array.length(),
    (array, index) -> array.getAndIncrement(index),
 array -> System.out.println(array)
 );

结果

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000] 

字段更新器

  • AtomicReferenceFieldUpdater // 域 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常

Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
 public class Test5 {
 private volatile int field;
 public static void main(String[] args) {
 AtomicIntegerFieldUpdater fieldUpdater =
 AtomicIntegerFieldUpdater.newUpdater(Test5.class, "field");
 Test5 test5 = new Test5();
 fieldUpdater.compareAndSet(test5, 0, 10);
 // 修改成功 field = 10
 System.out.println(test5.field);
 // 修改成功 field = 20
 fieldUpdater.compareAndSet(test5, 10, 20);
 System.out.println(test5.field);
 // 修改失败 field = 20
 fieldUpdater.compareAndSet(test5, 10, 30);
 System.out.println(test5.field);
    }
 }

输出

10 
20 
20 
  • 30
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

过去日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值