【多线程:cas】原子类型

简介: 【多线程:cas】原子类型

【多线程:cas】原子类型

01.解释

JUC提供了一些实现了cas的工具类:三大类 原子整数 原子引用 原子数组

02.原子整数

有AtomicBoolean、AtomicInteger、AtomicLong,这里我们拿AtomicInteger举例

        AtomicInteger i = new AtomicInteger(5);

        System.out.println(i.incrementAndGet()); // ++i
        System.out.println(i.getAndIncrement()); // i++

        System.out.println(i.decrementAndGet()); // --i
        System.out.println(i.getAndDecrement()); // i--

        System.out.println(i.getAndAdd(5)); // 先打印再自增5
        System.out.println(i.addAndGet(5)); // 先自增5再打印

        i.updateAndGet(value -> value * 10);
        // 内部是函数式接口 可以使用lambda表达式
        // 可以自定义运算方式,更新但没有返回值
        i.getAndUpdate(value -> value * 10);
        // 返回更新后的返回值

上述都是AtomicInteger类常用的方法 方法对应的操作都是原子的 不会出现线程安全问题 注释中有对应的操作。

03.原子引用

补充

在学下面内容之前,我们要明确一个概念,cas操作中的比较的是地址值或者地址的内容?答案是地址值,下面的ABA问题会体现。

为什么要有原子引用类型

我们知道类型分为基本类型(int long char等)和引用类型(类对象),上述我们的原子整数对应的就是基本类型的cas实现,现在的原子引用就是引用类型的cas实现。

AtomicReference< Object>

介绍
泛型里放要使用的引用类型 保证它的原子性
例子
还是上一篇文章的取钱例子,只不过把int类型换为了BigDecimal类型,我们现在是否还会出现线程安全问题
代码

@Slf4j(topic = "c.Test35")
public class Test35 {
    public static void main(String[] args) {
        DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
    }
}

class DecimalAccountCas implements DecimalAccount {
    private AtomicReference<BigDecimal> balance;

    public DecimalAccountCas(BigDecimal balance) {
//        this.balance = balance;
        this.balance = new AtomicReference<>(balance);
    }

    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
        while(true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.subtract(amount);
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

interface DecimalAccount {
    // 获取余额
    BigDecimal getBalance();

    // 取款
    void withdraw(BigDecimal amount);

    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(DecimalAccount account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(BigDecimal.TEN);
            }));
        }
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(account.getBalance());
    }
}

结果

0

解释
可以看出没有出现线程安全问题

ABA问题

介绍
ABA问题是指 我们现在有三个线程 t1 t2 t3,t1线程想要把字符A更新为C 但是在此之前t2线程把A更新为B,t3线程把B更新为A,问题是:此时t1线程的cas操作是否可以成功 我们上述已经介绍了cas操作比较的是地址 理论上来说cas操作不会成功,但是事实上这个操作是可以成功的

@Slf4j(topic = "c.TestABA")
public class TestABA {

    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();
    }
}

结果

15:25:51.824 c.TestABA [main] - main start...
15:25:51.865 c.TestABA [t1] - change A->B true
15:25:52.373 c.TestABA [t2] - change B->A true
15:25:53.381 c.TestABA [main] - change A->C true

解释
我们可以看见最后一次cas操作成功了,那有人会想应该这里比较的类型是String,我们又知道String类型的equals方法进行了重写 只要值相同算相同,但还有人认为这里比较不是用equals比较的而是用 == 比较的,比较的是地址 至于为什么相同是因为 我们的"A"是从常量池中获取的 也就是s1 == "A"与s2 == "A" 的地址值都是"A"的地址 所以比较才会相同 进而交换成功。

探究cas操作比较的是equals还是==

还是上面那个代码 只不过这次我们直接创建一个String对象 而不是采用从常量池获取的方式,看看这次的ABA是否会成功

@Slf4j(topic = "c.TestABA")
public class TestABA {

    static AtomicReference<String> ref = new AtomicReference<>(new String("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, new String("C")));
    }

    private static void other() {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.get(), new String("B")));
        }, "t1").start();
        sleep(0.5);
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.get(), new String("A")));
        }, "t2").start();
    }
}

结果

15:36:40.437 c.TestABA [main] - main start...
15:36:40.473 c.TestABA [t1] - change A->B true
15:36:40.981 c.TestABA [t2] - change B->A true
15:36:41.985 c.TestABA [main] - change A->C false

解释
我们发现这次cas操作竟然失败了,说明了什么?说明cas操作比较的是 == 而不是equals,因为如果是equals 那么s1 == new String("A")与s2 == new String("A")的比较结果应该相同 cas操作应该成功。综上我们得出了cas操作比较的是 ==

如何解决ABA问题:AtomicStampedReference

介绍
AtomicStampedReference是AtomicReference的升级,它加入了计时器(版本号) 每进行一次操作计时器就会改变,最后进行cas对比时 比较的不仅仅是地址还有计时器是否相同
对上述代码优化

@Slf4j(topic = "c.Test36")
public class Test36 {

    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:42:58.652 c.Test36 [main] - main start...
15:42:58.655 c.Test36 [main] - 版本 0
15:42:58.709 c.Test36 [t1] - change A->B true
15:42:58.709 c.Test36 [t1] - 更新版本为 1
15:42:59.213 c.Test36 [t2] - change B->A true
15:42:59.214 c.Test36 [t2] - 更新版本为 2
15:43:00.227 c.Test36 [main] - change A->C false

解释
我们发现这次交换失败,原因就是上面所讲增加了计时器,之后的每一次cas操作不仅需要传 最新值和要更新的值 还需要传 版本号和要更新的版本号,最终解决了ABA问题。

注意

因为基本类型的地址值就是它们的值本身 所以原子整数这一类 也会发生ABA问题

AtomicMarkableReference

AtomicMarkableReference方法和AtomicStampedReference基本一致,不同的是AtomicMarkableReference方法把计时器改为了boolean类型 也就是标记,如果改过就改变标记 最后比较时 比较一下标记是否一样,不过这里有一个问题 就是 因为boolean只有两种状态true与false 也就是它只能保证解决AB问题或者降低ABA问题出现的几率,但是不能解决ABA问题。
AtomicMarkableReference方法因为和AtomicStampedReference方法用法基本一致这里不再举例。

04.原子数组

AtomicIntegerArray:原子整型数组
AtomicLongArray:原子长整型数组
AtomicReferenceArray:原子引用数组
这里我们拿AtomicIntegerArray举例

介绍

为什么需要原子数组,因为我们之前的数组在新增元素的时候是线程不安全的。

原子数组使用

例子介绍
创建十个线程 与一个 公共的int数组长度为10,每个线程分别往对应的数组下标元素里面加10000次1,例如线程0就向a[0]加1,最后打印数组,理论上的结果应该是数组的十个元素都是10000。
代码
下面代码用到了lambda函数式接口的知识,如果不会的同学可以看我之前的lambda表达式的文章
https://blog.csdn.net/m0_71229547/article/details/125046910?spm=1001.2014.3001.5501
https://blog.csdn.net/m0_71229547/article/details/125093487?spm=1001.2014.3001.5501

public class Test39 {

    public static void main(String[] args) {
        demo(
                ()->new int[10],
                (array)->array.length,
                (array, index) -> array[index]++,
                array-> System.out.println(Arrays.toString(array))
        );

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

    /**
     参数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);
    }
}

结果

[9335, 9298, 9263, 9254, 9318, 9260, 9248, 9229, 9230, 9242]
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

解释
可以看出普通数组存在并发安全问题,但是原子数组并没有这样的问题。

原子数组的ABA问题

ABA出现的原因
我们先来分析为什么会有ABA问题,究其根本就是cas比较时地址没有改变 但其实以及改变过了。
原子数组的cas实现

我们注意到compareAndSet的3个参数,i代表的是数组偏移值 expect代码的是数组偏移值对应元素的旧值 update代表的是偏移值对应元素的要更新的值。
我们注意几个点

1.原子数组cas比较的是数组里面偏移值对应的元素,而不是数组与数组之间的比较
2.既然是数组元素的比较,所以因为数组类型的不同就有可能出现ABA问题

例子

@Slf4j(topic = "c.TestABA")
public class TestABA {

    static AtomicIntegerArray ref = new AtomicIntegerArray(10);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        int prev = ref.get(1);
        other();
        sleep(1);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(1,prev,2));
    }

    private static void other() {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(1,ref.get(1),1));
        }, "t1").start();
        sleep(0.5);
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(1,ref.get(1), 0));
        }, "t2").start();
    }
}

结果

16:41:43.219 c.TestABA [main] - main start...
16:41:43.274 c.TestABA [t1] - change 0->1 true
16:41:43.790 c.TestABA [t2] - change 1->0 true
16:41:44.790 c.TestABA [main] - change 0->2 true

解释
可以看出用原子整数数组类型 发生了ABA问题,究其根本就是因为AtomicIntegerArray内部实现是int为基本类型所以会出现cas比较成功的问题,那我们用AtomicReferenceArray 原子引用数组 会有这个问题吗?一样会有

如何解决原子数组的ABA问题
AtomicReferenceArray<>的泛型中填AtomicStampedReference类型,这样保证在数组元素比较时 有版本号保证 不会出现ABA问题。

目录
相关文章
|
Java 索引
多线程和并发编程(2)—CAS和Atomic实现的非阻塞同步
多线程和并发编程(2)—CAS和Atomic实现的非阻塞同步
87 1
多线程和并发编程(2)—CAS和Atomic实现的非阻塞同步
|
Java 编译器
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
|
6月前
|
算法 安全 Java
Java多线程基础-12:详解CAS算法
CAS(Compare and Swap)算法是一种无锁同步原语,用于在多线程环境中更新内存位置的值。
68 0
|
1月前
|
安全
【多线程】CAS、ABA问题详解
【多线程】CAS、ABA问题详解
21 0
|
6月前
|
存储 安全 Java
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
106 3
|
6月前
|
安全 Java 编译器
Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)
Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)
|
6月前
|
安全 Java
多线程(CAS, ABA问题, Runnable & Callable & 僵尸线程 & 孤儿进程)
多线程(CAS, ABA问题, Runnable & Callable & 僵尸线程 & 孤儿进程)
60 1
|
6月前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
63 0
|
6月前
|
安全 Java API
JavaEE多线程】深入理解CAS操作:无锁编程的核心
JavaEE多线程】深入理解CAS操作:无锁编程的核心
48 0

热门文章

最新文章