百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

JUC包中的分而治之策略-为提高性能而生

lipiwang 2025-06-15 17:23 2 浏览 0 评论

一、前言

本次分享我们来共同探讨JUC包中一些有意思的类,包含AtomicLong & LongAdder,ThreadLocalRandom原理。

二、AtomicLong & LongAdder

2.1 AtomicLong 类

AtomicLong是JUC包提供的原子性操作类,其内部通过CAS保证了对计数的原子性更新操作。

大家可以翻看源码发现内部是通过UnSafe(rt.jar)这个类的CAs操作来保证对内部的计数器变量 long value进行原子性更新的,比如JDK8中:

    public final long incrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
    }

其中unsafe.getAndAddLong的代码如下:

  public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
    {
      l = getLongVolatile(paramObject, paramLong1);//(1)
    } while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));//(2)
    return l;
  }

可知最终调用的是native 方法compareAndSwapLong原子性操作。

当多个线程调用同一个AtomicLong实例的incrementAndGet方法后,多个线程都会执行到unsafe.getAndAddLong方法,然后多个线程都会执行到代码(1)处获取计数器的值,然后都会去执行代码(2),如果多个线程同时执行了代码(2),由于CAS具有原子性,所以只有一个线程会更新成功,然后返回true从而退出循环,整个更新操作就OK了。其他线程则CAS失败返回false,则循环一次在次从(1)处获取当前计数器的值,然后在尝试执行(2),这叫做CAS的自旋操作,本质是使用Cpu 资源换取使用锁带来的上下文切换等开销。

2.2 LongAdder类

AtomicLong类为开发人员使用线程安全的计数器提供了方便,但是AtomicLong在高并发下存在一些问题,如上所述,当大量线程调用同一个AtomicLong的实例的方法时候,同时只有一个线程会CAS计数器的值成功,失败的线程则会原地占用cpu进行自旋转重试,这回造成大量线程白白浪费cpu原地自旋转。

在JDK8中新增了一个LongAdder类,其采用分而治之的策略来减少同一个变量的并发竞争度,LongAdder的核心思想是把一个原子变量分解为多个变量,让同样多的线程去竞争多个资源,这样竞争每个资源的线程数就被分担了下来,下面通过图形来理解下两者设计的不同之处:

如上图AtomicLong是多个线程同时竞争同一个原子变量。

如上图LongAdder内部维护多个Cell变量,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这是变相的减少了争夺共享资源的并发量。

下面我们首先看下Cell的结构:

    @sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe 机制
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base,由于Cells占用内存是相对比较大的,所以一开始并不创建,而是在需要时候在创建,也就是惰性 创建。

当一开始判断cell数组是null并且并发线程较少时候所有的累加操作都是对base变量进行的,这时候就退化为了AtomicLong。cell数组的大小保持是2的N次方大小,初始化时候Cell数组的中Cell的元素个数为2,数组里面的变量实体是Cell类型。

当多个线程在争夺同一个Cell原子变量时候如果失败并不是在当前cell变量上一直自旋CAS重试,而是会尝试在其它Cell的变量上进行CAS尝试,这个改变增加了当前线程重试时候CAS成功的可能性。最后获取LongAdder当前值的时候是把所有Cell变量的value值累加后在加上base返回的,如下代码:

    public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

如上代码可知首先把base的值赋值给sum变量,然后通过循环把每个cell元素的value值累加到sum变量上,最后返回sum.

其实这是一种分而治之的策略,先把并发量分担到多个原子变量上,让多个线程并发的对不同的原子变量进行操作,然后获取计数时候在把所有原子变量的计数和累加。

思考问题:

  • 何时初始化cell数组
  • 当前线程如何选择cell中的元素进行访问
  • 如果保证cell中元素更新的线程安全
  • cell数组何时进行扩容,cell元素个数可以无限扩张?

性能对比,这里有一个文章
http://blog.palominolabs.com/2014/02/10/java-8-performance-improvements-longadder-vs-atomiclong/

三、 Random & ThreadLocalRandom

3.1 Random类原理及其局限性

在JDK7之前包括现在java.util.Random应该是使用比较广泛的随机数生成工具类,下面先通过简单的代码看看java.util.Random是如何使用的:

public class RandomTest {
    public static void main(String[] args) {

        //(1)创建一个默认种子的随机数生成器
        Random random = new Random();
        //(2)输出10个在0-5(包含0,不包含5)之间的随机数
        for (int i = 0; i < 10; ++i) {
            System.out.println(random.nextInt(5));
        }
    }
}
  • 代码(1)创建一个默认随机数生成器,使用默认的种子。
  • 代码(2)输出输出10个在0-5(包含0,不包含5)之间的随机数。
    public int nextInt(int bound) {
        //(3)参数检查
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        //(4)根据老的种子生成新的种子
        int r = next(31);
        //(5)根据新的种子计算随机数
        ...
        return r;
    } 

如上代码可知新的随机数的生成需要两个步骤

  • 首先需要根据老的种子计算生成新的种子。
  • 然后根据新的种子和bound变量通过一定的算法来计算新的随机数。

下面看下next()代码:

    protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            //(6)获取当前原子变量种子的值
            oldseed = seed.get();
            //(7)根据当前种子值计算新的种子
            nextseed = (oldseed * multiplier + addend) & mask;
            //(8)使用新种子替换老的种子
        } while (!seed.compareAndSet(oldseed, nextseed));
        //(9)
        return (int)(nextseed >>> (48 - bits));
    }
  • 代码(6)使用原子变量的get方法获取当前原子变量种子的值
  • 代码(7)根据具体的算法使用当前种子值计算新的种子
  • 代码(8)使用CAS操作,使用新的种子去更新老的种子,多线程下可能多个线程都同时执行到了代码(6)那么可能多个线程都拿到的当前种子的值是同一个,然后执行步骤(7)计算的新种子也都是一样的,但是步骤(8)的CAS操作会保证只有一个线程可以更新老的种子为新的,失败的线程会通过循环从新获取更新后的种子作为当前种子去计算老的种子,这就保证了随机数的随机性。
  • 代码(9)则使用固定算法根据新的种子计算随机数,并返回。

3.2 ThreadLocalRandom

Random类生成随机数原理以及不足:每个Random实例里面有一个原子性的种子变量用来记录当前的种子的值,当要生成新的随机数时候要根据当前种子计算新的种子并更新回原子变量。

多线程下使用单个Random实例生成随机数时候,多个线程同时计算随机数计算新的种子时候多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,那么CAS操作失败的大量线程进行自旋重试,而大量线程的自旋重试是会降低并发性能和消耗CPU资源的,为了解决这个问题,ThreadLocalRandom类应运而生。

public class RandomTest {

    public static void main(String[] args) {
        //(10)获取一个随机数生成器
        ThreadLocalRandom random =  ThreadLocalRandom.current();

        //(11)输出10个在0-5(包含0,不包含5)之间的随机数
        for (int i = 0; i < 10; ++i) {
            System.out.println(random.nextInt(5));
        }
    }
}

如上代码(10)调用ThreadLocalRandom.current()来获取当前线程的随机数生成器。下面来分析下ThreadLocalRandom的实现原理。

从名字看会让我们联想到ThreadLocal类。ThreadLocal通过让每一个线程拷贝一份变量,每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝,从而避免了对共享变量进行同步。实际上ThreadLocalRandom的实现也是这个原理。Random的缺点是多个线程会使用原子性种子变量,会导致对原子变量更新的竞争,这个原理可以通过下面图来表达:

那么如果每个线程维护自己的一个种子变量,每个线程生成随机数时候根据自己本地内存中的老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题,这会大大提高并发性能,如下图ThreadLocalRandom原理可以使用下图表达:

Thread类里面有几个变量:

    /** The current seed for a ThreadLocalRandom */
    @sun.misc.Contended("tlr")
    long threadLocalRandomSeed;

    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

思考问题:

  • 每个线程的初始种子怎么生成的
  • 如果保障多个线程产生的种子不一样

四、总结

本文摘自 Java 并发编程之美 一书,具体可以单击阅读原文。其实JUC包中还有其他一些经典的组件,比如fork-join框架等,这个后面有时间我们在一块讨论。

相关推荐

软件测试|MySQL CROSS JOIN:交叉连接的详细解析

简介在MySQL数据库中,CROSSJOIN是一种用于生成两个或多个表的笛卡尔积的连接方法。CROSSJOIN不需要任何连接条件,它将左表的每一行与右表的每一行进行组合,从而生成一个包含所...

「MySQL笔记」left join-on-and 与 left join-on-where 的区别

1.摘要关于这两种写法的重要知识点摘要如下:left-join时,即使有相同的查询条件,二者的查询结果集也不同,原因是优先级导致的,on的优先级比where高on-and是进行韦恩运算连接...

MySQL中的JOIN——联合查询的基本语法

MySQL中的JOIN指令用来将两个或多个表中的数据进行联合查询,根据连接条件来匹配记录,从而得到需要的结果集。在MySQL中,常见的JOIN类型包括INNERJOIN、LEFTJOIN和RIGH...

MySQL 中的 CROSS JOIN:强大的连接工具

CROSSJOIN在MySQL里是一种挺特别的连接操作,它能弄出连接表的笛卡尔积。这就是说,要是表A有m行,表B有n行,那ACROSSJOINB的结果就会有m*n...

大厂必问:MySQL 三表 JOIN 操作的解析与性能优化,效率又如何?

大厂必问:MySQL三表JOIN操作的解析与性能优化策略,效率又如何?点击关注,开启技术之旅!大家好,这里是互联网技术学堂,无论你是一名程序员、设计师、还是对技术充满好奇心的普通人,都欢迎你加入...

面试题:MySQL 的 JOIN 查询优化(mysql查询优化方法)

MySQL的JOIN查询优化是提升数据库性能的关键环节。以下是综合多个技术文档的核心优化策略,按优先级和实现难度分类:一、索引优化:性能提升的基础为连接字段建立索引确保参与JOIN的列(通常...

Flink中处理维表关联技术实现路径

在Flink中处理维表关联大体氛围TableSQLLookupJoin和DataStream算子函数,主要技术实现路径:I.FlinkSQL/TableAPI中的Lookup...

深入剖析Zookeeper原理(一)整体设计

1.ZK集群架构设计与特性1.ZK集群架构设计:ZK主要分为三种角色:Leader(领导者):一个Zookeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follwer及...

多种负载均衡算法及其Java代码实现

首先给大家介绍下什么是负载均衡负载均衡建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。负载均衡,英...

一分钟了解SpringCloud中的ribbon到底是什么,原理是啥?

1.概念ribbon是一款客户端负载均衡器,用于微服务之间的负载均衡。首先,什么是客户端负载均衡?如图,ribbon可以通过注册中心获取服务列表,然后自己执行自己的负载均衡策略来决定要访问哪个微服务,...

Step by Step之腾讯云短信-验证码实践

在商城小程序和前端上线用了一阵子之后,用户提出了体验提升的需求,如忘记密码、绑定用户、快捷注册等,作为业界最佳实践的短信验证码登录、重置密码和注册等功能开发也就提上日程了,本文就以重置密码为例,将验证...

10分钟入门响应式:Springboot整合kafka实现reactive

Springboot引入Reactor已经有一段时间了,笔者潜伏在各种技术群里暗中观察发现,好像scala圈子的同仁们,似乎对响应式更热衷一点。也许是因为他们对fp理解的更深吧,所以领悟起来障碍性更少...

使用java随机生成有个性的用户名,LOL地名+水浒传,合计2808个

*随机生成用户名*取水浒传108好汉名字*取LOL地名26个,组合而成*一共可以生成2808个不同特色的用户名如果你在上网的时候,用户名难取的话,这里有很多可选择的用户名,现提供100个...

深入理解Math.random()的概率分布特性

直接上源码/***Returnsa{@codedouble}valuewithapositivesign,*返回一个带符号的double类型的数字,说人话就是返回一个非负...

编程英文 - 创建/生成/构建 (create/generate/build)

在软件开发中,create、generate和build这三个词经常被用到,它们都与"创造"或"产生"某些东西有关,但在具体使用场景和含义上有所不同。基本含义creat...

取消回复欢迎 发表评论: