共享模型之无锁(二)

news2025/3/14 3:19:41

1.原子基本类型

1>.J.U.C并发包提供了多个原子基本类型:

AtomicBoolean
AtomicInteger
AtomicLong
...

2>.以AtomicInteger为例:

public class TestAtomicIntegerDemo01 {
    public static void main(String[] args) {
        //原子整型类
        AtomicInteger i = new AtomicInteger(0);

        // 先获取再自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
        System.out.println(i.getAndIncrement()); //0

        // 先自增再获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
        System.out.println(i.incrementAndGet()); //2

        // 先自减再获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
        System.out.println(i.decrementAndGet()); //1

        // 先获取再自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
        System.out.println(i.getAndDecrement()); //1

        // 先获取再加值(i = 0, 结果 i = 5, 返回 0)
        System.out.println(i.getAndAdd(5)); //0

        // 先加值再获取(i = 5, 结果 i = 0, 返回 0)
        System.out.println(i.addAndGet(-5)); //0

        // 先获取再更新(i = 0, v 为 i 的当前值, 结果 i = -2, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(i.getAndUpdate(v -> v - 2)); //0

        // 先更新再获取(i = -2, v 为 i 的当前值, 结果 i = 0, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(i.updateAndGet(v -> v + 2)); //0

        // 先获取再计算(i = 0, v 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        // getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
        // getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
        System.out.println(i.getAndAccumulate(10, (v, x) -> v + x)); //0

        // 先计算再获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(i.accumulateAndGet(-10, (v, x) -> v + x)); //0
    }
}

2.原子引用类型

1>.为什么需要原子引用类型?

因为程序中要保护的共享数据并不一定都是基本数据类型,也有对象类型,此时就需要通过原子引用类型进行保护;

2>.J.U.C并发包提供了多个原子引用类型:

AtomicReference
AtomicMarkableReference
AtomicStampedReference
...

3>.以AtomicReference为例:

public class TestAtomicReferenceDemo1 {
    public static void main(String[] args) {
        DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("200")));
    }
}

class DecimalAccountCas implements DecimalAccount {

    //余额,共享变量,通过原子引用类型保护某个基本类型的共享数据
    //将基本数据类型引用本身进行修改
    private AtomicReference<BigDecimal> balance;

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

    //获取余额
    @Override
    public BigDecimal getBalance() {
        return this.balance.get();
    }

    //取款
    @Override
    public void withdraw(BigDecimal amount) {
        while (true) {
            //获取(最新)余额
            BigDecimal prev = this.balance.get();
            //取款计算
            BigDecimal next = prev.subtract(amount);
            //将取款后的余额通过CAS机制更新到主内存中
            if (this.balance.compareAndSet(prev, next)) {
                //更新成功,结束循环;否则,继续重试;
                break;
            }
        }
    }
}

interface DecimalAccount {

    // 获取余额
    BigDecimal getBalance();

    // 取款
    void withdraw(BigDecimal amount);

    /**
     * 方法内会启动 20 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 200 那么正确的结果应当是 0
     */
    static void demo(DecimalAccount account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 20; 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());
    }
}

在这里插入图片描述

3.ABA问题及解决

3.1.ABA问题描述

1>.CAS算法实现的一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,这个时间差(当前线程修改数据期间)会导致数据的变化;

假设如下事件序列:有1,2两个线程,线程1耗时长,线程2耗时时间短:

  • ①.线程1从内存位置V中取出A;
  • ②.线程2从内存位置V中取出A;
  • ③.线程2进行了一些操作,通过CAS操作将B(操作后的值)写入位置V,此时主内存中的值由A变成了B;
  • ④.线程2通过CAS操作将A再次写入位置V,此时主内存中变量的值由B又恢复到A了;
  • ⑤.线程1进行CAS操作,通过比较发现位置V中仍然是A,然后替换操作成功,但其实主内存中变量V的值(在线程1修改变量V期间)已经被修改过了,虽然值还是一样的,但是毕竟被其他线程修改过了,只是线程1不知道而已,这并不符合JMM的内存可见性;

尽管线程1的CAS操作成功,但不代表这个过程没有问题——对于线程1来说,线程2对变量的修改已经丢失了!

2>.代码如下:

@Slf4j
public class TestABAProblemDemo1 {
    //原子引用类型保护共享变量
    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        log.info("main start...");
        // 获取值 A
        // 这个共享变量被它线程修改过?
        String prev = ref.get();
        // 如果中间有其它线程干扰,发生了ABA现象
        other();
        TimeUnit.SECONDS.sleep(2);
        // 尝试改为 C
        log.info("change A->C {}", ref.compareAndSet(prev, "C"));
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            log.info("change A->B {}", ref.compareAndSet(ref.get(), "B"));
        }, "t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            log.info("change B->A {}", ref.compareAndSet(ref.get(), "A"));
        }, "t2").start();
    }
}

在这里插入图片描述

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

3.2.ABA问题解决方案一

1>.使用一个带时间戳的原子引用类型--AtomicStampedReference

@Slf4j
public class TestABAProblemDemo1 {
    //使用一个带时间戳的原子引用类型保护共享变量,时间戳默认值为0或者1;
    static AtomicStampedReference<String> ref = new AtomicStampedReference<String>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        log.info("main start...");
        // 获取原始值A
        String prev = ref.getReference();
        // 获取主内存中共享数据此时版本号
        log.info("修改之前的版本{}", ref.getStamp());

        other();
        TimeUnit.SECONDS.sleep(2);

        // 尝试改为 C
        // 此时不仅仅是比较共享变量的值,还要比较stamp版本号,而且修改完成之后stamp版本号还要加1,表示当前线程已经修改了该共享变量;
        log.info("修改之前的版本{}", ref.getStamp());
        log.info("change A->C {}", ref.compareAndSet(prev, "C", ref.getStamp(), ref.getStamp() + 1));
        log.info("修改之后的版本{}", ref.getStamp());
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            // 此时不仅仅是比较共享变量的值,还要比较stamp版本号,而且修改完成之后stamp版本号还要加1,表示当前线程已经修改了该共享变量;
            log.info("修改之前的版本{}", ref.getStamp());
            log.info("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
            log.info("修改之后的版本{}", ref.getStamp());
        }, "t1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            // 此时不仅仅是比较共享变量的值,还要比较stamp版本号,而且修改完成之后stamp版本号还要加1,表示当前线程已经修改了该共享变量;
            log.info("修改之前的版本{}", ref.getStamp());
            log.info("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
            log.info("修改之后的版本{}", ref.getStamp());
        }, "t2").start();
    }
}

在这里插入图片描述
2>.分析:

①.AtomicStampedReference可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A -> C,通过AtomicStampedReference我们可以知道,引用变量中途被更改了几次;
②.但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference原子标记引用类型;

3.3.ABA问题解决方案二

1>.使用可以标记共享变量是否发生过修改的原子标记引用类型--AtomicMarkableReference

@Slf4j
public class TestABAProblemDemo2 {
    public static void main(String[] args) throws InterruptedException {
        GarbageBag bag = new GarbageBag("(旧)垃圾袋装满了垃圾");
        // 参数2 mark可以看作一个标记,表示垃圾袋满了!
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);

        log.info("主线程 start...");
        GarbageBag prev = ref.getReference();
        log.info(prev.toString());

        new Thread(() -> {
            log.info("打扫卫生的线程 start...,将垃圾袋中的垃圾全部倒掉...");
            bag.setDesc("(旧)空垃圾袋");

            //还是使用之前的旧垃圾袋
            //第一个bag是期望值,第二个bag是目标值,这里的两个bag都是同一个对象的实例!
            while (!ref.compareAndSet(bag, bag, true, false)) {}
            log.info(bag.toString());
        },"t1").start();

        TimeUnit.SECONDS.sleep(1);

        log.info("主线程想换一只新垃圾袋?");
        boolean success = ref.compareAndSet(prev, new GarbageBag("(新)空垃圾袋"), true, false);
        //由于主线程中已经修改了共享数据,对象的初始标记发生了变化,这里再去修改就会失败!
        log.info("换了么?" + success);
        log.info(ref.getReference().toString());
    }
}

@Data
@NoArgsConstructor
@Accessors(chain = true)
class GarbageBag {
    String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }
}

在这里插入图片描述
2>.分析:
在这里插入图片描述

4.原子数组

1>.如果要修改的不是(类型)引用本身,而是要修改引用对象里面(内部)的内容.例如数组,有些时候多个线程并不是要修改数组的引用地址,而是要修改数组内存储的元素,此时之前的原子引用类型对象就无法实现了,但是别担心,JUC提供了原子数组,保护数组元素在多线程环境下的线程安全!

AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray

2>.代码示例:

public class TestAtomicArray {
    public static void main(String[] args) {
        demo(
            () -> new AtomicIntegerArray(10),
            (array) -> array.length(),
            (array, index) -> array.incrementAndGet(index),
            (array) -> System.out.println(array)
        );
    }

    /**
     * 参数1,提供数组、可以是线程不安全数组或线程安全数组
     * 参数2,获取数组长度的方法
     * 参数3,元素自增方法
     * 参数4,打印数组的方法
     */
    // supplier 提供者 无中生有 ()->结果
    // function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
    // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->void
    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 < 10; 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);
    }
}

在这里插入图片描述

数组中的元素都加到了10,这是正确的!

5.原子更新器(字段更新器)

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

Exception in thread “main” java.lang.IllegalArgumentException: Must be volatile type

2>.JUC提供的字段更新器对象:

AtomicReferenceFieldUpdater //引用类型字段
AtomicIntegerFieldUpdater   //整型字段
AtomicLongFieldUpdater      //长整形字段

3>.示例代码:

@Slf4j
public class TestAtomicFieldUpdater {
    public static void main(String[] args) {
        Student student = new Student();

        //为某个类中某个类型的字段创建更新器对象
        AtomicReferenceFieldUpdater referenceFieldUpdater =
            AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
        //更新对象中的某个属性值
        boolean flag = referenceFieldUpdater.compareAndSet(student, null, "张三");
        if (flag){
            log.info(student.getName());  //张三
        }
    }
}

@Data
@NoArgsConstructor
class Student {
   volatile String name;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/335077.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux入门---基础指令(上)

这里写目录标题前言ls指令pwd指令cd指令touch指令mkdirrmdirrmman指令cp指令mv指令前言 我们平时使用电脑主要是通过鼠标键盘以及操作系统中自带的图形来对电脑执行相应的命令&#xff0c;比如说我想打开D盘中的cctalk这个文件&#xff1a; 我就可以先用鼠标左键单击这个文件…

负载均衡的方式

在业务初期&#xff0c;我们一般会先使用单台服务器对外提供服务。随着业务流量越来越大&#xff0c;单台服务器无论如何优化&#xff0c;无论采用多好的硬件&#xff0c;总会有性能天花板&#xff0c;当单服务器的性能无法满足业务需求时&#xff0c;就需要把多台服务器组成集…

五岳科技与亚马逊云科技,助力中国产品实现全球品牌力提升

随着DTC模式实践在全球跨境电商市场取得成功&#xff0c;越来越多中国品牌走出国门&#xff0c;走向世界。而文化差异、语言隔阂、信息差等始终是行业中的共同难题&#xff0c;如何提高竞争壁垒与解决数据困境成为企业的共同需求。 作为一家致力于用AI技术赋能传统行业升级以…

将群晖NAS变为本地盘

本文介绍一个工具&#xff0c;可以在 Windows 系统下将群晖NAS的目录变为本地盘&#xff0c;好处是在外部访问的时候&#xff0c;能够大大改善体验。可以用本地的应用程序直接打开&#xff0c;速度依赖网络带宽&#xff0c;正常情况下&#xff0c;看视频是没有问题的。当然&…

MySQL入门篇-Xtrabackup详细介绍

Xtrabackup简介 MySQL冷备、mysqldump、MySQL热拷贝都无法实现对数据库进行增量备份。在实际生产环境中增量备份是非常实用的&#xff0c;如果数据大于50G或100G&#xff0c;存储空间足够的情况下&#xff0c;可以每天进行完整备份&#xff0c;如果每天产生的数据量较大&#…

Vue3 企业级优雅实战 - 组件库框架 - 11 组件库的打包构建和发布

回顾第一篇文章中谈到的组件库的几个方面&#xff0c;只剩下最后的、也是最重要的组件库的打包构建、本地发布、远程发布了。 1 组件库构建 组件库的入口是 packages/yyg-demo-ui&#xff0c;构建组件库有两个步骤&#xff1a; 添加 TypeScript 的配置文件&#xff1a; tsco…

百趣代谢组学资讯:槟榔的基因组为雌雄同株植物的性别决定提供见解

文章标题&#xff1a;The genome of Areca catechu provides insights into sex determination of monoecious plants 发表期刊&#xff1a;New Phytologist 影响因子&#xff1a;10.323 作者单位&#xff1a;海南大学 百趣生物提供服务&#xff1a;植物激素高通量靶标定…

怎么查看自己的电脑IP地址?

作为一个互联网冲浪侠&#xff0c;你应该对IP地址并不陌生&#xff1a;访问网站和网络服务器知道你的IP地址&#xff1b;发送的电子邮件头包含你的IP地址。如果有人想从IP地址追踪到你的物理地址和身份&#xff0c;是有可能的。 IP地址代表互联网协议地址。它是一个特殊的号码…

linux高级命令系列一

重定向命令学习目标能够使用重定向命令将终端显示内容重定向到文件1. 重定向命令的介绍重定向也称为输出重定向&#xff0c;把在终端执行命令的结果保存到目标文件。2. 重定向命令的使用命令说明>如果文件存在会覆盖原有文件内容&#xff0c;相当于文件操作中的‘w’模式>…

C/C++:预处理(下)

目录 一.回顾程序的编译链接过程 二. 预处理之预定义#define 1.#define定义的标识符 2.#define定义的宏 3.带副作用的表达式作为宏实参 4.两个经典的宏 5.#define使用的一些注意事项小结 6.宏与函数的比较 7.#undef 附&#xff1a;关于#define的三个冷知识 三. 条件…

Android 14 首个开发者预览版到来

作者 / Dave Burke, VP of Engineering让 Android 在数十亿用户的手中良好运行&#xff0c;是我们、Android 设备制造商&#xff0c;以及开发者社区的一致追求。今天&#xff0c;我们为大家带来 Android 14 的第一个开发者预览版。大家针对预览版提出的反馈具有重要意义&#x…

企业管理的三大基石及其关系

企业管理的三大基石三大基石是什么三大基石的关系制度&#xff1a;管理&#xff1a;文化&#xff1a;三大基石是什么 一个企业&#xff0c;不管它是属于哪种类型&#xff0c;影响员工行为的都有三种力量——制度、管理和文化&#xff0c;这是管理的三大基石。 三大基石的关系 …

sleep()、wait()、 join()、 yield()的区别

在这之前&#xff0c;需要明白锁池和等待池的含义 1.锁池 所有需要竞争同步锁的线程都会放在锁池当中&#xff0c;比如当前对象的锁已经被其中一个线程得到&#xff0c;则其他线程需要在这个锁池进行等待&#xff0c;当前面的线程释放同步锁后锁池中的线程去竞争同步锁&#…

ThreadLocal 源码级别详解

ThreadLocal简介 稍微翻译一下&#xff1a; ThreadLocal提供线程局部变量。这些变量与正常的变量不同&#xff0c;因为每一个线程在访问ThreadLocal实例的时候&#xff08;通过其get或set方法&#xff09;都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静…

excel拆分实例:如何快速制作考勤统计分析表

面对新的统计需求&#xff0c;很多人会一下变懵&#xff0c;不知如何办。如果涉及的统计有一千多行数据&#xff0c;哭的心思都有了&#xff1a;什么时候才能下班哟&#xff01;今天老菜鸟通过考勤统计分析表实例分享自己面对新统计需求的解决方法&#xff1a;简化数据、找数据…

【表面缺陷检测】基于YOLOX的PCB表面缺陷检测(全网最详细的YOLOX保姆级教程)

写在前面: 首先感谢兄弟们的订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。 Hello,大家好,我是augustqi。 今天给大家分享一个表面缺陷检测项目:基于YOLOX的PCB表面缺陷检测(保姆级教程)。多的…

用于异常检测的深度神经网络模型融合

用于异常检测的深度神经网络模型融合 在当今的数字时代&#xff0c;网络安全至关重要&#xff0c;因为全球数十亿台计算机通过网络连接。近年来&#xff0c;网络攻击的数量大幅增加。因此&#xff0c;网络威胁检测旨在通过观察一段时间内的流量数据来检测这些攻击&#xff0c;…

主流无线音频传输方案

一、概述 无线音频传输很大程度上解决了音影设备布线难的问题&#xff0c;特别是大型的场合。科技的进步&#xff0c;用户对无线传输的要求越来越高&#xff0c;一发对多收的无线音频方案将成为主流。 二、方案分类 无线传输方案&#xff0c;从目前来说方案的种类还是很多的&am…

线材分享丨同为(TOWE)IEC 60320国际标准制式电源转换延长线

电源线的作用是传输电流&#xff0c;其传输方式通常是点对点传输&#xff0c;在生活中我们随处可见它的身影。电源线按照用途可以分为AC交流电源线及DC直流电源线&#xff0c;而AC电源线有着高需要统一标准获得安全认证&#xff0c;如国标CCC认证机构、美国UL认证机构、欧洲VDE…

Java笔记-异常相关

一、异常概述与异常体系结构 Error:Java虚拟机无法解决的严重问题&#xff1a; JVM系统内部错误&#xff0c;资源耗尽&#xff0c;如&#xff1a;StackOverflow \OOM堆栈溢出 处理办法&#xff1a;只能修改代码&#xff0c;不能编写处理异常的代码 Exception:可以处理的异常 &…