【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了(中)

简介: 【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了(中)

CAS失败—什么都不做


这个我就不再画图,说白了就是Z线程进来后,发现预期值和内存值不一样的时候,就什么都不做,就CAS失败,直接结束掉线程了。这个有些场景也会这么去干

CAS为什么是原子的呢?


有的人可能会问:CAS明明就有多部操作,但什么就是原子的呢?

解释如下:


Unsafe底层实际上是调用C代码,C代码调用汇编,最后生成出一条CPU指令cmpxchg,完成操作。这也就为啥CAS是原子性的,因为它是一条CPU指令,不会被打断。


CAS是原子性的,虽然你可能看到比较后再修改(compare and swap)觉得会有两个操作,但终究是原子性的


CAS带来的ABA问题


什么是ABA问题呢?结束上面的例子


1.线程A和线程C同时读到count变量,所以线程A和线程C的内存值和预期值都为10


2.此时线程A使用CAS将count值修改成100


3.修改完后,就在这时,线程B进来了(因为CPU随机,所以是有可能先执行B再执行C的),读取得到count的值为100(内存值和预期值都是100),将count值修改成10


4.线程C拿到执行权,发现内存值是10,预期值也是10,将count值修改成11


产生的问题是:线程C无法得知线程A和线程B修改过的count值,这样是有风险的。,如下:

场景:蛋糕店回馈客户,对于会员卡余额小于20的客户一次性赠送20,刺激消费,每个客户只能赠送一次


    public static void main(String[] args) {
         //在这里使用AtomicReference  里面装着用户的余额  初始卡余额小于20
        final AtomicReference<Integer> money = new AtomicReference<>(19);
        //模拟一个生产者消费者模型
        // 模拟多个线程更新数据库,为用户充值
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                while (true) {
                    while (true) {
                        Integer m = money.get();
                        if (m < 20) {
                            if (money.compareAndSet(m, m + 20)) {
                                System.out.println("余额小于20,充值成功。余额:"
                                        + money.get() + "元");
                                break;
                            }
                        } else {
                            System.out.println("余额大于20,无需充值!");
                            break;
                        }
                    }
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        // 用户消费进程,模拟消费行为
        new Thread(() -> {
            //在这里的for循环,太快很容易看不到结果
            for (int i = 0; i < 1000; i++) {
                while (true) {
                    Integer m = money.get();
                    if (m > 10) {
                        System.out.println("大于10元");
                        if (money.compareAndSet(m, m - 10)) {
                            System.out.println("成功消费10,卡余额:" + money.get());
                            break;
                        }
                    } else {
                        System.out.println("余额不足!");
                        break;
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }


输出:


余额小于20,充值成功。余额:39元
余额大于20,无需充值!
余额大于20,无需充值!
大于10元
成功消费10,卡余额:29
大于10元
成功消费10,卡余额:19
大于10元
成功消费10,卡余额:9
余额小于20,充值成功。余额:29元
余额大于20,无需充值!
余额大于20,无需充值!
大于10元
成功消费10,卡余额:19
大于10元
成功消费10,卡余额:9
余额不足!
余额大于20,无需充值!
余额大于20,无需充值!
余额小于20,充值成功。余额:29元


我们看到,这个帐号先后反复多次进行充值。,怎么回事呢?


原因是帐户余额被反复修改,修改后的值等于原来的值,使得CAS操作无法正确判断当前的数据状态。这在业务上是不允许的(只有高并发下才可能会出现哦,并不是说记录下赠送次数就能简单解决的哦)。


ABA问题如何解决



其实java也考虑到了这个问题,所以提供给予我们解决方案了


我们可以使用JDK给我们提供的AtomicStampedReference和AtomicMarkableReference类。


用代码解决上面的充值问题:该动起来也是非常的简单

   public static void main(String[] args) {
        //在这里使用AtomicReference  里面装着用户的余额  初始卡余额小于20
        final AtomicStampedReference<Integer> money = new AtomicStampedReference<>(19, 0);
        for (int i = 0; i < 3; i++) {
            //拿到当前的版本号
            final int timestamp = money.getStamp();
            new Thread(() -> {
                while (true) {
                    while (true) {
                        Integer m = money.getReference();
                        if (m < 20) {
                            //注意此处:timestamp版本号做了+1操作
                            if (money.compareAndSet(m, m + 20, timestamp,
                                    timestamp + 1)) {
                                System.out.println("余额小于20,充值成功。余额:"
                                        + money.getReference() + "元");
                                break;
                            }
                        } else {
                            System.out.println("余额大于20,无需充值!");
                            break;
                        }
                    }
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        // 用户消费进程,模拟消费行为
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                while (true) {
                    //拿到当前的版本号
                    int timestamp = money.getStamp();
                    Integer m = money.getReference();
                    if (m > 10) {
                        System.out.println("大于10元");
                        if (money.compareAndSet(m, m - 10, timestamp,
                                timestamp + 1)) {
                            System.out.println("成功消费10,卡余额:"
                                    + money.getReference());
                            break;
                        }
                    } else {
                        System.out.println("余额不足!");
                        break;
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }


运行看输出结果为:


余额小于20,充值成功。余额:39元
余额大于20,无需充值!
余额大于20,无需充值!
大于10元
成功消费10,卡余额:29
大于10元
成功消费10,卡余额:19
大于10元
成功消费10,卡余额:9
余额不足!
余额不足!
余额不足!
余额不足!


我们发现,只为他充值了一次,之后一直消费都是余额不足的状态了。因此当高并发又可能存在ABA的情况下,这样就能彻底杜绝问题了


简单来说就是在给为这个对象提供了一个版本,并且这个版本如果被修改了,是自动更新的。原理大概就是:维护了一个Pair对象,Pair对象存储我们的对象引用和一个stamp值。每次CAS比较的是两个Pair对象

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }


Atomic原子变量类的使用


java.util.concurrent.atomic原子操作包为我们提供了四类原子操作:

提供类如下截图:


image.png


1.原子更新基本类型

AtomicBoolean:布尔型

AtomicInteger:整型

AtomicLong:长整型


2.原子更新数组

AtomicIntegerArray:数组里的整型

AtomicLongArray:数组里的长整型

AtomicReferenceArray:数组里的引用类型


3.原子更新引用

AtomicReference<V>:引用类型

AtomicStampedReference:带有版本号的引用类型(可以防止ABA问题)

AtomicMarkableReference:带有标记位的引用类型


4.原子更新字段

AtomicIntegerFieldUpdater:对象的属性是整型

AtomicLongFieldUpdater:对象的属性是长整型

AtomicReferenceFieldUpdater:对象的属性是引用类型


5.JDK8新增

DoubleAccumulator、LongAccumulator、

DoubleAdder、LongAdder


是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。

相关文章
|
13天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
40 4
|
1月前
|
算法 Java 程序员
Java中的Synchronized,你了解多少?
Java中的Synchronized,你了解多少?
|
1月前
|
Java
让星星⭐月亮告诉你,Java synchronized(*.class) synchronized 方法 synchronized(this)分析
本文通过Java代码示例,介绍了`synchronized`关键字在类和实例方法上的使用。总结了三种情况:1) 类级别的锁,多个实例对象在同一时刻只能有一个获取锁;2) 实例方法级别的锁,多个实例对象可以同时执行;3) 同一实例对象的多个线程,同一时刻只能有一个线程执行同步方法。
18 1
|
1月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
24 2
|
1月前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
16 0
|
2月前
|
存储 安全 Java
Java并发编程之深入理解Synchronized关键字
在Java的并发编程领域,synchronized关键字扮演着守护者的角色。它确保了多个线程访问共享资源时的同步性和安全性。本文将通过浅显易懂的语言和实例,带你一步步了解synchronized的神秘面纱,从基本使用到底层原理,再到它的优化技巧,让你在编写高效安全的多线程代码时更加得心应手。
|
2月前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。
|
3月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
28 2
|
3月前
|
传感器 C# 监控
硬件交互新体验:WPF与传感器的完美结合——从初始化串行端口到读取温度数据,一步步教你打造实时监控的智能应用
【8月更文挑战第31天】本文通过详细教程,指导Windows Presentation Foundation (WPF) 开发者如何读取并处理温度传感器数据,增强应用程序的功能性和用户体验。首先,通过`.NET Framework`的`Serial Port`类实现与传感器的串行通信;接着,创建WPF界面显示实时数据;最后,提供示例代码说明如何初始化串行端口及读取数据。无论哪种传感器,只要支持串行通信,均可采用类似方法集成到WPF应用中。适合希望掌握硬件交互技术的WPF开发者参考。
68 0
|
3月前
|
安全 Java
Java并发编程实战:使用synchronized和ReentrantLock实现线程安全
【8月更文挑战第31天】在Java并发编程中,保证线程安全是至关重要的。本文将通过对比synchronized和ReentrantLock两种锁机制,深入探讨它们在实现线程安全方面的优缺点,并通过代码示例展示如何使用这两种锁来保护共享资源。