Java——聊聊JUC中的原子变量类

news2024/12/23 12:31:47

文章目录:

        1.什么是原子变量类?

        2.AtomicInteger(基本类型原子变量类)

        3.AtomicIntegerArray(数组类型原子变量类)

        4.AtomicMarkableReference(引用类型原子变量类)

        5.AtomicIntegerFieldUpdater(对象Integer类型属性修改原子变量类)

        6.AtomicReferenceFieldUpdater(对象引用类型属性修改原子变量类)

        7.LongAdder、LongAccumulator(原子变量增强类)

        8.浅谈LongAdder为什么这么快?


1.什么是原子变量类?

我们参照jdk的软件包,可以看到就是在 java.util.concurrent.atomic 包下。

一共16个原子变量类,下面我来通过一些Demo简单介绍一下它们的用法。


2.AtomicInteger(基本类型原子变量类)

AtomicInteger 和 AtomicLong 以及 AtomicBoolean 都是一个类别的,都是操作单个数据,只是类型不一样(int、long、布尔)。

所以我就以AtomicInteger举例。

package com.juc.atomic;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author: SongZiHao
 * @date: 2023/2/11
 */
class MyNumber {
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addPlusPlus() {
        atomicInteger.getAndIncrement();
    }
}

public class AtomicIntegerDemo {
    public static final int SIZE = 50;

    public static void main(String[] args) throws InterruptedException {
        MyNumber number = new MyNumber();
        CountDownLatch cdl = new CountDownLatch(SIZE); //计数器
        for (int i = 0; i < SIZE; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
                        number.addPlusPlus(); //50个线程,每个线程执行1000次number++自增操作
                    }
                } finally {
                    cdl.countDown(); //每执行完一个线程,计数器减一
                }
            }, String.valueOf(i)).start();
        }
        cdl.await(); //这里阻塞等待,直到50个线程全部执行完,计数器清零,程序才会继续向下执行
        System.out.println(Thread.currentThread().getName() + " result: " + number.atomicInteger.get());
    }
}


3.AtomicIntegerArray(数组类型原子变量类)

AtomicIntegerArray 和 AtomicLongArray 以及 AtomicReferenceArray 都是一个类别的,都是操作数组类型数据,只是数组的类型不一样(int、long、引用类型)。

所以我就以 AtomicIntegerArray 举例。

package com.juc.atomic;

import java.util.concurrent.atomic.AtomicIntegerArray;

/**
 * @author: SongZiHao
 * @date: 2023/2/11
 */
public class AtomicIntegerArrayDemo {
    public static void main(String[] args) {
        AtomicIntegerArray array = new AtomicIntegerArray(new int[5]); //0 0 0 0 0
//        AtomicIntegerArray array = new AtomicIntegerArray(5); //0 0 0 0 0
//        AtomicIntegerArray array = new AtomicIntegerArray(new int[]{1, 2, 3, 4, 5}); //1 2 3 4 5
        for (int i = 0; i < array.length(); i++) {
            System.out.println(array.get(i)); //0 0 0 0 0
        }
        int ans = 0;
        ans = array.getAndSet(0, 666); //先get,后set
        System.out.println(ans + ", " + array.get(0));
        ans = array.getAndIncrement(0); //先get,后自增(i++)
        System.out.println(ans + ", " + array.get(0));
    }
}


4.AtomicMarkableReference(引用类型原子变量类)

AtomicReference:可以带泛型,更新引用类型。

AtomicStampedReference:携带版本号的引用类型原子类,解决修改过几次的问题,可以解决ABA问题。关于这个原子变量类的Demo可以参考我的这篇文章:CAS解决ABA问题

AtomicMarkableReference:类似于AtomicStampedReference,原子更新带有标记位的引用类型对象,只是不采用版本号记录,而是采用标记位true、false。

来看下面关于AtomicMarkableReference的Demo。

package com.juc.atomic;

import java.util.concurrent.atomic.AtomicMarkableReference;

/**
 * @author: SongZiHao
 * @date: 2023/2/11
 */
public class AtomicMarkableReferenceDemo {
    static AtomicMarkableReference markableReference = new AtomicMarkableReference(100, false);

    public static void main(String[] args) {
        new Thread(() -> {
            boolean marked = markableReference.isMarked(); //false
            System.out.println(Thread.currentThread().getName() + " 默认标识:" + marked);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //1秒之后,t1线程读取值是100,和预期一样,首先将marked由false改为取反之后的值,也即true
            markableReference.compareAndSet(100, 1000, marked, !marked);
        }, "t1").start();
        new Thread(() -> {
            boolean marked = markableReference.isMarked(); //false
            System.out.println(Thread.currentThread().getName() + " 默认标识:" + marked);
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //2秒之后,t2再次读取,值已经被改为了1000,marked也被改为了true,所以此次cas失败
            boolean flag = markableReference.compareAndSet(100, 2000, marked, !marked);
            System.out.println(Thread.currentThread().getName() + " cas-result:" + flag); //cas失败,所以是false
            System.out.println(Thread.currentThread().getName() + " " + markableReference.isMarked()); //marked已被t1线程改为true
            System.out.println(Thread.currentThread().getName() + " " + markableReference.getReference()); //变量值已被t1线程改为1000
        }, "t2").start();
    }
}


5.AtomicIntegerFieldUpdater(对象Integer类型属性修改原子变量类)

AtomicIntegerFieldUpdater:原子更新对象中Integer类型字段的值,该字段必须以 volatile int 修饰。

AtomicLongFieldUpdater:原子更新对象中Long类型字段的值,该字段必须以 volatile long 修饰。

AtomicReferenceFieldUpdater:原子更新引用类型字段的值,该字段必须以 volatile T 修饰。(T是引用类型的泛型值)

因为对象的属性修改类型原子类都是抽象类*,所以每次使用都必须使用静态方法newUpdater()创建一个更新器*,并且需要设置想要更新的类和属性。

下面先看 AtomicIntegerFieldUpdater 的Demo。

package com.juc.atomic;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

/**
 * @author: SongZiHao
 * @date: 2023/2/11
 */
class BankAccount {
    public String bankName = "CCB";
    public volatile int money = 0;

    AtomicIntegerFieldUpdater<BankAccount> fieldUpdater =
            AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");

    public void addMoney(BankAccount bankAccount) {
        fieldUpdater.getAndIncrement(bankAccount);
    }
}

public class AtomicIntegerFieldUpdateDemo {
    public static void main(String[] args) throws InterruptedException {
        BankAccount bankAccount = new BankAccount();
        CountDownLatch cdl = new CountDownLatch(10); //计数器
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
                        bankAccount.addMoney(bankAccount); //10个线程,每个线程对money执行1000次自增操作
                    }
                } finally {
                    cdl.countDown(); //每执行完一个线程,计数器减一
                }
            }, String.valueOf(i)).start();
        }
        cdl.await(); //阻塞等待,直到10个线程全部执行完,计数器清零,程序继续向下执行
        System.out.println(Thread.currentThread().getName() + " result: " + bankAccount.money);
    }
}


6.AtomicReferenceFieldUpdater(对象引用类型属性修改原子变量类)

上面介绍了AtomicIntegerFieldUpdater针对Integer类型的属性进行修改。

下面来看 AtomicReferenceFieldUpdater 如何针对引用类型的属性进行修改。

package com.juc.atomic;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * @author: SongZiHao
 * @date: 2023/2/11
 */
class Resource {
    public volatile Boolean isInit = Boolean.FALSE;

    AtomicReferenceFieldUpdater<Resource, Boolean> fieldUpdater
            = AtomicReferenceFieldUpdater.newUpdater(Resource.class, Boolean.class, "isInit");

    public void init(Resource resource) {
        //cas操作,同一时刻只会有一个线程cas成功,其他线程cas失败或者自旋等待
        if (fieldUpdater.compareAndSet(resource, Boolean.FALSE, Boolean.TRUE)) {
            System.out.println(Thread.currentThread().getName() + " ---- start init, need 1 second");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " ---- end init....");
        } else {
            System.out.println("已经有其他线程在进行初始化操作....");
        }
    }
}

public class AtomicReferenceFieldUpdateDemo {
    public static void main(String[] args) {
        Resource resource = new Resource();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                resource.init(resource); //5个线程并发去修改资源类中的引用类型属性
            }, String.valueOf(i)).start();
        }
    }
}

由于以上两个案例都是针对某个类中的某个属性进行原子修改,而且这些属性都采用 volatile 修饰,小提一嘴:

面试官问你:你在哪里用了volatile?

  1. 在AtomicReferenceFieldUpdater中,因为是规定好的必须由volatile修饰的。
  2. 单例模式的DCL写法中,采用volatile保证单例在多线程之间的可见性。

7.LongAdder、LongAccumulator(原子变量增强类)

首先,我们来看在阿里巴巴Java开发手册中有这样一个参考内容。他说的是 LongAdder 要比传统的 AtomicLong 的性能更好,同时也会减少乐观锁的重试次数(这个很关键,因为我们都知道原子变量类的底层实现都是CAS,而CAS就是基于乐观锁机制做的)。

下面我们先看一下简单的案例。

package com.juc.atomic;

import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

/**
 * @author: SongZiHao
 * @date: 2023/2/11
 */
public class LongAdderDemo {
    public static void main(String[] args) {
        LongAdder longAdder = new LongAdder(); //0
        longAdder.increment();
        longAdder.increment();
        longAdder.increment(); //3
        System.out.println(longAdder.sum());

        LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
        longAccumulator.accumulate(1); //此时x=0,y=1,求和结果是1
        longAccumulator.accumulate(3); //此时x=1,y=3,求和结果是4
        System.out.println(longAccumulator.get()); //4
    }
}

上面的案例就不多说了,主要来看一下下面关于 synchronized、AtomicLong、LongAdder、LongAccumulator 在多线程并发情况下的性能对比(一目了然)。

模拟的是一个点赞器功能,50个线程,每个线程点赞100万次,最终数据一定是五千万。

package com.juc.atomic;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

/**
 * @author: SongZiHao
 * @date: 2023/2/11
 */
class ClickNumber {
    int number = 0;
    public synchronized void addBySynchronized() {
        number++;
    }

    AtomicLong atomicLong = new AtomicLong(0);
    public void addByAtomicLong() {
        atomicLong.getAndIncrement();
    }

    LongAdder longAdder = new LongAdder();
    public void addByLongAdder() {
        longAdder.increment();
    }

    LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
    public void addByLongAccumulator() {
        longAccumulator.accumulate(1);
    }
}

public class LongAccumulatorDemo {
    public static final int CLICK_NUMBER = 1000000;
    public static final int THREAD_NUMBER = 50;

    public static void main(String[] args) throws InterruptedException {
        ClickNumber clickNumber = new ClickNumber();
        long startTime;
        long endTime;
        CountDownLatch cdl1 = new CountDownLatch(THREAD_NUMBER);
        CountDownLatch cdl2 = new CountDownLatch(THREAD_NUMBER);
        CountDownLatch cdl3 = new CountDownLatch(THREAD_NUMBER);
        CountDownLatch cdl4 = new CountDownLatch(THREAD_NUMBER);

        //synchronized
        startTime = System.currentTimeMillis();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < CLICK_NUMBER; j++) {
                        clickNumber.addBySynchronized();
                    }
                } finally {
                    cdl1.countDown();
                }
            }, String.valueOf(i)).start();
        }
        cdl1.await();
        endTime = System.currentTimeMillis();
        System.out.println("synchronized result: " + clickNumber.number + ", cost time: " + (endTime - startTime) + " ms....");

        //AtomicLong
        startTime = System.currentTimeMillis();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < CLICK_NUMBER; j++) {
                        clickNumber.addByAtomicLong();
                    }
                } finally {
                    cdl2.countDown();
                }
            }, String.valueOf(i)).start();
        }
        cdl2.await();
        endTime = System.currentTimeMillis();
        System.out.println("AtomicLong result: " + clickNumber.atomicLong.get() + ", cost time: " + (endTime - startTime) + " ms....");

        //LongAdder
        startTime = System.currentTimeMillis();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < CLICK_NUMBER; j++) {
                        clickNumber.addByLongAdder();
                    }
                } finally {
                    cdl3.countDown();
                }
            }, String.valueOf(i)).start();
        }
        cdl3.await();
        endTime = System.currentTimeMillis();
        System.out.println("LongAdder result: " + clickNumber.longAdder.sum() + ", cost time: " + (endTime - startTime) + " ms....");

        //LongAccumulator
        startTime = System.currentTimeMillis();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < CLICK_NUMBER; j++) {
                        clickNumber.addByLongAccumulator();
                    }
                } finally {
                    cdl4.countDown();
                }
            }, String.valueOf(i)).start();
        }
        cdl4.await();
        endTime = System.currentTimeMillis();
        System.out.println("LongAccumulator result: " + clickNumber.longAccumulator.get() + ", cost time: " + (endTime - startTime) + " ms....");
    }
}

从结果中可以看到, 内部锁synchronized是最耗时的,因为它的锁粒度比较粗,不再多说了。AtomicLong基于CAS乐观锁,性能要好一些。而两个原子变量增强类的性能可以说在AtomicLong基础上提升了将近10倍,这如果是在高并发的场景下就很恐怖了。。。


8.浅谈LongAdder为什么这么快?

我们浅谈一下LongAdder在大并发的情况下,性能为什么这么快?

其实在小并发下情况差不多;但在高并发情况下,在AtomicLong中,等待的线程会不停的自旋,导致效率比较低;而LongAdder用cell[]分了几个块出来,最后统计总的结果值(base+所有的cell值),分散热点。

  • 内部有一个base变量,一个Cell[]数组。
  • base变量:非竞态条件下,直接累加到该变量上。
  • Cell[]数组:竞态条件下,累加各个线程自己的槽Cell[i]中。

举个形象的例子,火车站买火车票,AtomicLong 只要一个窗口,其他人都在排队;而LongAdder 利用cell开了多个卖票窗口,所以效率高了很多。

LongAdder的基本思路就是分散热点 ,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点 。

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

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

相关文章

二叉树OJ题(上)

✅每日一练&#xff1a;100. 相同的树 - 力扣&#xff08;LeetCode&#xff09; 题目的意思是俩棵树的结构不仅要相同&#xff0c;而且每个节点的值还要相同&#xff0c;如果满足上面2个条件&#xff0c;则成立&#xff01; 解题思路&#xff1a; 从三个方面去考虑&#xff1…

分布式之分布式事务V2

写在前面 本文一起来看下分布式环境下的事务问题&#xff0c;即我们经常听到的分布式事务问题。想要解决分布式事务问题&#xff0c;需要使用到分布式事务相关的协议&#xff0c;主要有2PC即两阶段提交协议&#xff0c;TCC&#xff08;try-confirm-cancel&#xff09;&#xf…

FPGA产业发展现状及人才培养研究报告

文章目录一、FPGA赋能智能时代二、FPGA市场现状及挑战2.1 FPGA市场发展现状2.2 FPGA主要应用场景2.3 人才问题成为FPGA发展的桎梏三、FPGA人才需求与人才培养3.1 FPGA人才需求特征3.2 FPGA人才培养现状3.2.1 培养主体3.2.2 培养机制3.2.3 培养人才的目的和宗旨3.2.4 FPGA人才培…

【C++】六个默认成员函数——取地址重载,const成员函数

&#x1f345; 初始化和清理 拷贝复制 目录 ☃️1.取地址重载 ☃️2.const取地址操作符重载 这两个运算符一般不需要重载&#xff0c;使用编译器生成的默认取地址的重载即可&#xff0c;只有特殊情况&#xff0c;才需要重载&#xff0c;比如想让别人获取到指定的内容&#xf…

计算机网络3:HTTP1.0和HTTP1.1的区别

目录1. HTTP是什么2.HTTP1.0和HTTP1.1的区别3.名词解释&#xff08;1&#xff09;If-Modified-Since&#xff08;IMS&#xff09;、Expires&#xff08;2&#xff09;If-None-Match&#xff0c;Etag&#xff08;3&#xff09;If-Unmodified-Since1. HTTP是什么 超文本传输协议…

2023全新SF授权系统源码 V3.7全开源无加密版本,亲测可用

2023全新SF授权系统源码 V3.7全开源无加密版本。网站搭建很简单&#xff0c;大致看来一下应该域名解析后上传源码解压&#xff0c;访问域名/install就能直接安装。 程序功能简介: 1.盗版入库(26种) 2.快捷登录 3.采用layuiadmin框架 4.易支付认证功能 5.程序自带商城系统…

SSO(单点登陆)

Single Sign On 一处登陆、处处可用 0、前置概念&#xff1a; 1&#xff09;、单点登录业务介绍 早期单一服务器&#xff0c;用户认证。 缺点&#xff1a;单点性能压力&#xff0c;无法扩展 分布式&#xff0c; SSO(single sign on)模式 解决 &#xff1a; 用户身份信息独…

微信小程序Springboot vue停车场车位管理系统

系统分为用户和管理员两个角色 用户的主要功能有&#xff1a; 1.用户注册和登陆系统 2.用户查看系统的公告信息 3.用户查看车位信息&#xff0c;在线预约车位 4.用户交流论坛&#xff0c;发布交流信息&#xff0c;在线评论 5.用户查看地图信息&#xff0c;在线导航 6.用户查看个…

Win11自定义电脑右下角时间显示格式

Win11自定义电脑右下角时间显示格式 一、进入附加设置菜单 1、进入控制面板&#xff0c;选择日期和时间 2、选择修改日期和时间 3、选择修改日历设置 4、选择附加设置 二、自定义时间显示出秒 1、在选项卡中&#xff0c;选时间选项卡 2、在Short time的输入框中输入H:m…

家政服务小程序实战教程04-页面传参及表单容器

我们在上一篇已经介绍了在生命周期函数中预加载会员信息&#xff0c;首次使用小程序的用户需要进行注册&#xff0c;注册的时候需要选择对应的角色&#xff0c;本篇我们就介绍会员注册的功能。 01 创建页面 会员注册&#xff0c;我们分两个页面&#xff0c;一个是角色选择页面…

VSCode Markdown写作引入符合规范的参考文献

Markdown可以用来写论文&#xff0c;写论文的时候无一例外要用到参考文献&#xff0c;今天来谈谈怎么自动生成参考文献。之前讲了怎么导出的pdf&#xff0c;文章在这里 VSCode vscode-pandoc插件将中文Markdown转换为好看的pdf文档&#xff08;使用eisvogel模板&#xff09; …

CMake中target_precompile_headers的使用

CMake中的target_precompile_headers命令用于添加要预编译的头文件列表&#xff0c;其格式如下&#xff1a; target_precompile_headers(<target><INTERFACE|PUBLIC|PRIVATE> [header1...][<INTERFACE|PUBLIC|PRIVATE> [header2...] ...]) # 1 target_preco…

select 与 where、group by、order by、limit 子句执行优先级比较

当 select 和 其他三种语句的一者或者多者同时出现时&#xff0c;他们之间是存在执行先后顺序的。 他们的优先级顺序是&#xff1a;where > group by > select > order by > limit 目录 1、select 与 where 2、group by 与 where 、select 2、select 与 order…

【Call for papers】CRYPTO-2023(CCF-A/网络与信息安全/2023年2月16日截稿)

Crypto 2023 will take place in Santa Barbara, USA on August 19-24, 2023. Crypto 2023 is organized by the International Association for Cryptologic Research (IACR). The proceedings will be published by Springer in the LNCS series. 文章目录1.会议信息2.时间节…

C++定位new用法及注意事项

使用定位new创建对象&#xff0c;显式调用析构函数是必须的&#xff0c;这是析构函数必须被显式调用的少数情形之一&#xff01;&#xff0c; 另有一点&#xff01;&#xff01;&#xff01;析构函数的调用必须与对象的构造顺序相反&#xff01;切记&#xff01;&#xff01;&a…

分步骤详解随机生成一个登录验证码的算法,最后给出完整代码

需要安装第三方模块pillow import randomfrom PIL import Image, ImageDraw, ImageFont步骤一&#xff1a;编写一个生成随机颜色的函数 def get_random_color():return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)步骤二&#xff1a;在面板里放…

31 岁生日快乐,Linux!

Linux 迎来了 31 岁生日&#xff0c;所以和我一起庆祝 Linux 的 31 岁生日吧&#xff0c;喝上一杯好香槟和一个美味的蛋糕&#xff01;虽然有些人不承认 8 月 25 日是 Linux 的生日&#xff0c;但我知道。1991 年 8 月 25 日&#xff0c;21 岁的芬兰学生 Linus Benedict Torval…

三种方式查看linux终端terminal是否可以访问外网ping,curl,wget

方法1&#xff1a;ping注意不要用ping www.google.com.hk来验证&#xff0c;因为有墙&#xff0c;墙阻止了你接受网址发回的响应数据。即使你那啥过&#xff0c;浏览器都可以访问Google&#xff0c;terminal里面也是无法得到响应 百度在墙内&#xff0c;所以可以正常拿到响应信…

二分法攻略

本节内容只有通过例题来记录效果才是最好的,请看下面内容&#xff01; 递归实现二分法 经典二分查找问题&#xff1a;LintCode 炼码 描述**&#xff1a;**在一个排序数组中找一个数&#xff0c;返回该数出现的任意位置&#xff0c;如果不存在&#xff0c;返回 -1。 输入&…

java spring完全注解开发

其实学习注解之后 我们也只有一个开启扫描需要xml的配置了 而 这一步 其实也是可以写到类里面的 我们这边先创建一个java项目 然后引入 spring需要的几个基本包 在src中创建一个包 叫 Bean Bean下创建 一个包 叫 UserData UserData 包下创建一个类 我这里叫 User 参考代码如…