到处都需要计数器,例如,查找应用程序的关键KPI,应用程序的负载,服务的请求总数,用于查找应用程序吞吐量的一些KPI等。
由于所有这些需求,并发复杂性也增加了,这使这个问题变得有趣。
如何实现并发计数器
- 同步 –这是JDK 1.5之前的唯一选项,因为现在我们正在等待JDK8版本,因此绝对不是一个选项。
- 基于锁 –切勿尝试将其用作计数器,否则它将性能很差
- 等待免费 -Java不支持Fetch-and-add ,因此实现起来有点困难。
- 无锁 –很好地支持“ 比较和交换” ,使用起来不错。
基于比较和交换的计数器如何执行
我使用AtomicInteger进行此测试,并且每个线程使此计数器递增1百万次,以增加线程的争用数量,并逐渐增加。
试验机详细信息
作业系统:Windows 8
JDK:1.7.0.25
处理器:Intel i7-3632QM,8 Core
内存:8 GB
Y轴 –增加一百万次所需的时间
X轴–螺纹数
随着线程数量的增加,增加计数器所花费的时间也增加了,这是由于争用造成的。 对于基于CAS的计数器,是CAS故障导致减速。
这是我们可以获得的最佳性能吗? 肯定不是它们是实现并发计数器的更好的解决方案,让我们来看看它们。
备用并发计数器
让我们看一些实现计数器的解决方案,以更好的方式处理竞争:
- 基于核心的计数器 –维护每个逻辑核心的计数器,这样您的争用就会减少。 您拥有这种类型的计数器的唯一问题是,如果线程数大于逻辑核心数,那么您将开始注意到竞争。
- 基于线程的计数器 –维护将使用系统的线程总数的计数器。 当线程数大于逻辑核心数时,这很好地工作。
让我们测试一下
不同类型的计数器花费的时间
Y轴 –增加一百万次所需的时间
X轴–螺纹数
同时用计数器进行比基于原子计数器要好得多,16个线程是5倍左右的时间比较好,这是巨大的差异!
CAS失效率
Y轴 – CAS失效100Ks
X轴–螺纹数
由于争用,基于原子的计数器会出现很多故障,并且随着我添加更多线程和其他计数器的性能提高,它会呈指数增长。
观察
多核计算机变得易于使用,并且我们必须改变处理并发的方式,传统的并发方式在当今时代无法扩展,因为拥有24或48核服务器非常普遍。
- 为了减少争用,您必须使用多个计数器,然后再将它们聚合
- 如果线程数少于或等于内核数,则基于内核的计数器效果很好
- 当线程数量远大于可用内核时,基于线程的计数器就很好
- 减少争用的关键是确定要写入哪个线程的计数器。我使用了基于线程ID的简单方法,但是可以使用更好的方法,请查看JDK 8的ThreadLocalRandom以获得一些想法。
- JDK8的LongAdder使用基于线程的方法,该方法创建许多插槽以减少争用。
Github提供了此测试中使用的所有计数器的代码
翻译自: https://www.javacodegeeks.com/2013/09/scalable-counters-for-multi-core.html