AtomicInteger 在高并发下性能不好,如何解决?以及为什么会出现这种情况?

我们知道在 JDK1.5 中新增了并发情况下使用的 Integer/Long 所对应的原子类 AtomicInteger 和 AtomicLong。

在并发的场景下,如果我们需要实现计数器,可以利用 AtomicInteger 和 AtomicLong,这样一来,就可以避免加锁和复杂的代码逻辑,有了它们之后,我们只需要执行对应的封装好的方法,例如对这两个变量进行原子的增操作或原子的减操作,就可以满足大部分业务场景的需求。

不过,虽然它们很好用,但是如果你的业务场景是并发量很大的,那么你也会发现,这两个原子类实际上会有较大的性能问题,这是为什么呢?就让我们从一个例子看起。

AtomicLong 存在的问题
首先我们来看一段代码:

复制代码
/**
* 描述:     在16个线程下使用AtomicLong
*/
public class AtomicLongDemo {
 
   public static void main(String[] args) throws InterruptedException {
       AtomicLong counter = new AtomicLong(0);
       ExecutorService service = Executors.newFixedThreadPool(16);
       for (int i = 0; i < 100; i++) {
           service.submit(new Task(counter));
       }
 
       Thread.sleep(2000);
       System.out.println(counter.get());
   }
 
   static class Task implements Runnable {
 
       private final AtomicLong counter;
 
       public Task(AtomicLong counter) {
           this.counter = counter;
       }
 
       @Override
       public void run() {
           counter.incrementAndGet();
       }
   }
}
在这段代码中可以看出,我们新建了一个原始值为 0 的 AtomicLong。然后,有一个线程数为 16 的线程池,并且往这个线程池中添加了 100 次相同的一个任务。

那我们往下看这个任务是什么。在下面的 Task 类中可以看到,这个任务实际上就是每一次去调用 AtomicLong 的 incrementAndGet 方法,相当于一次自加操作。这样一来,整个类的作用就是把这个原子类从 0 开始,添加 100 个任务,每个任务自加一次。

这段代码的运行结果毫无疑问是 100,虽然是多线程并发访问,但是 AtomicLong 依然可以保证 incrementAndGet 操作的原子性,所以不会发生线程安全问题。

不过如果我们深入一步去看内部情景的话,你可能会感到意外。我们把模型简化成只有两个线程在同时工作的并发场景,因为两个线程和更多个线程本质上是一样的。如图所示:

我们可以看到在这个图中,每一个线程是运行在自己的 core 中的,并且它们都有一个本地内存是自己独用的。在本地内存下方,有两个 CPU 核心共用的共享内存。

对于 AtomicLong 内部的 value 属性而言,也就是保存当前 AtomicLong 数值的属性,它是被 volatile 修饰的,所以它需要保证自身可见性。

这样一来,每一次它的数值有变化的时候,它都需要进行 flush 和 refresh。比如说,如果开始时,ctr 的数值为 0 的话,那么如图所示,一旦  core 1 把它改成 1 的话,它首先会在左侧把这个 1 的最新结果给 flush 到下方的共享内存。然后,再到右侧去往上 refresh 到核心 2 的本地内存。这样一来,对于核心 2 而言,它才能感知到这次变化。

由于竞争很激烈,这样的 flush 和 refresh 操作耗费了很多资源,而且 CAS 也会经常失败。

LongAdder 带来的改进和原理

在 JDK 8 中又新增了 LongAdder 这个类,这是一个针对 Long 类型的操作工具类。那么既然已经有了 AtomicLong,为何又要新增 LongAdder 这么一个类呢?

我们同样是用一个例子来说明。下面这个例子和刚才的例子很相似,只不过我们把工具类从  AtomicLong 变成了 LongAdder。其他的不同之处还在于最终打印结果的时候,调用的方法从原来的 get 变成了现在的 sum 方法。而其他的逻辑都一样。

我们来看一下使用 LongAdder 的代码示例:

复制代码
/**
* 描述:     在16个线程下使用LongAdder
*/
public class LongAdderDemo {
 
   public static void main(String[] args) throws InterruptedException {
       LongAdder counter = new LongAdder();
       ExecutorService service = Executors.newFixedThreadPool(16);
       for (int i = 0; i < 100; i++) {
           service.submit(new Task(counter));
       }
 
       Thread.sleep(2000);
       System.out.println(counter.sum());
   }
   static class Task implements Runnable {
 
       private final LongAdder counter;
 
       public Task(LongAdder counter) {
           this.counter = counter;
       }
 
       @Override
       public void run() {
           counter.increment();
       }
   }
}
代码的运行结果同样是 100,但是运行速度比刚才 AtomicLong 的实现要快。下面我们解释一下,为什么高并发下 LongAdder 比 AtomicLong 效率更高。

因为 LongAdder 引入了分段累加的概念,内部一共有两个参数参与计数:第一个叫作 base,它是一个变量,第二个是 Cell[] ,是一个数组。

其中的 base 是用在竞争不激烈的情况下的,可以直接把累加结果改到 base 变量上。

那么,当竞争激烈的时候,就要用到我们的 Cell[] 数组了。一旦竞争激烈,各个线程会分散累加到自己所对应的那个 Cell[] 数组的某一个对象中,而不会大家共用同一个。

这样一来,LongAdder 会把不同线程对应到不同的 Cell 上进行修改,降低了冲突的概率,这是一种分段的理念,提高了并发性,这就和 Java 7 的 ConcurrentHashMap 的 16 个 Segment 的思想类似。

竞争激烈的时候,LongAdder 会通过计算出每个线程的 hash 值来给线程分配到不同的 Cell 上去,每个 Cell 相当于是一个独立的计数器,这样一来就不会和其他的计数器干扰,Cell 之间并不存在竞争关系,所以在自加的过程中,就大大减少了刚才的 flush 和 refresh,以及降低了冲突的概率,这就是为什么 LongAdder 的吞吐量比 AtomicLong 大的原因,本质是空间换时间,因为它有多个计数器同时在工作,所以占用的内存也要相对更大一些。

那么 LongAdder 最终是如何实现多线程计数的呢?答案就在最后一步的求和 sum 方法,执行 LongAdder.sum() 的时候,会把各个线程里的 Cell 累计求和,并加上 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;
}
在这个 sum 方法中可以看到,思路非常清晰。先取 base 的值,然后遍历所有 Cell,把每个 Cell 的值都加上去,形成最终的总和。由于在统计的时候并没有进行加锁操作,所以这里得出的 sum 不一定是完全准确的,因为有可能在计算 sum 的过程中 Cell 的值被修改了。

那么我们已经了解了,为什么 AtomicLong 或者说 AtomicInteger 它在高并发下性能不好,也同时看到了性能更好的 LongAdder。下面我们就分析一下,对它们应该如何选择。

如何选择

在低竞争的情况下,AtomicLong 和 LongAdder 这两个类具有相似的特征,吞吐量也是相似的,因为竞争不高。但是在竞争激烈的情况下,LongAdder 的预期吞吐量要高得多,经过试验,LongAdder 的吞吐量大约是 AtomicLong 的十倍,不过凡事总要付出代价,LongAdder 在保证高效的同时,也需要消耗更多的空间。

AtomicLong 可否被 LongAdder 替代?

那么我们就要考虑了,有了更高效的 LongAdder,那 AtomicLong 可否不使用了呢?是否凡是用到 AtomicLong 的地方,都可以用 LongAdder 替换掉呢?答案是不是的,这需要区分场景。

LongAdder 只提供了 add、increment 等简单的方法,适合的是统计求和计数的场景,场景比较单一,而 AtomicLong 还具有 compareAndSet 等高级方法,可以应对除了加减之外的更复杂的需要 CAS 的场景。

结论:如果我们的场景仅仅是需要用到加和减操作的话,那么可以直接使用更高效的 LongAdder,但如果我们需要利用 CAS 比如 compareAndSet 等操作的话,就需要使用 AtomicLong 来完成。

引用:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=278

Java多线程学习二十七:AtomicInteger 在高并发下性能不好,如何解决?为什么?相关推荐

  1. Java多线程学习二十九:AtomicInteger(原子类) 和 synchronized 的异同点?

    原子类和 synchronized 关键字都可以用来保证线程安全,在本课时中,我们首先分别用原子类和 synchronized 关键字来解决一个经典的线程安全问题,给出具体的代码对比,然后再分析它们背 ...

  2. java多线程学习二、安全与不安全示例:12306买票和银行取钱、java内存模型、内存可见性、线程同步块和方法

    文章目录 前言 1. 什么是块,分为几种 2. 静态块与构造块的区别 一. 举例说明:并发情况下,线程不安全 1. 示例1:unsafe12306取票 2. 示例2:unsafe银行取钱 二.线程不安 ...

  3. Java多线程学习三十七:volatile 的作用是什么?与 synchronized 有什么异同

    volatile 是什么 首先我们就来介绍一下 volatile,它是 Java 中的一个关键字,是一种同步机制.当某个变量是共享变量,且这个变量是被 volatile 修饰的,那么在修改了这个变量的 ...

  4. Java多线程学习二十六:原子类是如何利用 CAS 保证线程安全的?

    什么是原子类,以及它有什么作用. 在编程领域里,原子性意味着"一组操作要么全都操作成功,要么全都失败,不能只操作成功其中的一部分".而 java.util.concurrent.a ...

  5. Java多线程学习(二)---线程创建方式

    线程创建方式 摘要: 1. 通过继承Thread类来创建并启动多线程的方式 2. 通过实现Runnable接口来创建并启动线程的方式 3. 通过实现Callable接口来创建并启动线程的方式 4. 总 ...

  6. Java多线程学习二十一:ConcurrentHashMap 在 Java7 和 8 有何不同

    在 Java 8 中,对于 ConcurrentHashMap 这个常用的工具类进行了很大的升级,对比之前 Java 7 版本在诸多方面都进行了调整和变化.不过,在 Java 7 中的 Segment ...

  7. Java多线程学习二十:HashMap 为什么是线程不安全的

    为什么 HashMap 是线程不安全的?而对于 HashMap,相信你一定并不陌生,HashMap 是我们平时工作和学习中用得非常非常多的一个容器,也是 Map 最主要的实现类之一,但是它自身并不具备 ...

  8. Java基础学习(二十七)之IO流

    1. File 1.1 File类概述和构造方法(myFile中的com.itheima_01中的FileDemo01) File:它是文件和目录路径名的抽象表示 文件和目录是可以通过File封装成对 ...

  9. Java多线程学习二十八:原子类和 volatile 有什么异同?

    原子类和 volatile 有什么异同 案例说明 volatile 和原子类的异同 我们首先看一个案例.如图所示,我们有两个线程. 在图中左上角可以看出,有一个公共的 boolean flag 标记位 ...

最新文章

  1. CCAH-CCA-500-4题:Where are Hadoop task log files stored?
  2. Apache常用配置
  3. mysql自增变量插入行时需要赋值
  4. C语言再学习 -- 位操作
  5. 面试题之丑数的C++实现求解(孤陋寡闻了,才知道丑数这么high的东东)
  6. 第三篇——第二部分——第一文 SQL Server镜像简介
  7. linux中wine yum安装,分享|在基于RedHat或Debian的系统上安装 Wine 1.7
  8. Tomcat的web项目部署方式
  9. PowerPoint笔记(四)
  10. 腾讯股价创新高:总裁刘炽平减持40万股 套现2.5亿港元
  11. 百分比怎么在java里显示_如何在java中找到百分比我的代码给出错误?
  12. JSK-373 加密【入门】
  13. “软”苹果水逆的一周:杂志服务崩溃,新机型遭泄露,芯片首架离职
  14. stm32中断优先级_STM的中断系统
  15. bzoj1783: [Usaco2010 Jan]Taking Turns
  16. 用Redis实现分布式锁 与 实现任务队列
  17. ap音频测试仪软件,美国进口音频分析仪/AP音频测试仪/电声测试仪
  18. Server2016系统关闭windows自动更新
  19. 华为鸿蒙操作系统国美通讯,国美通讯(600898)03月06日14:30大单揭秘
  20. 《大秦帝国》第四部,阵容太强大

热门文章

  1. 奔驰宝马路虎等齐降价 最高降幅达8.5万!
  2. 拳王公社:没钱没资源没人脉!网络创业凭副业年赚20W+!
  3. loadDataWithBaseURL加载HTML数据
  4. 开始用Tornado:从Hello World开始
  5. java servlet 开发_Java之基于Servlet的Web开发简介
  6. MySQL数据类型不一致_数据类型不一致导致的SQL不走索引
  7. linux目录结构与功能_深入理解linux系统的目录结构(总结的非常详细)
  8. 嵌入式Linux入门2:工具掌握(Windows篇)
  9. java任务追踪预警怎么写_分布式系统中如何优雅地追踪日志(原理篇)
  10. python条件语句有哪些_Python 条件语句