- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
ArrayList去重操作必备,附JMH性能分析!
这里提供四种实用的ArrayList去重的方法,同时我会实用Oracle提供的性能测试工具JMH(Java Microbenchmark Harness,Java微基准测试套件)来测试一下这几种方法的性能。当然这里测试的是在系统性能充裕的情况下进行的,看一看谁更能压榨机器的性能。
ArrayList去重常用方法分为四种,分别是 HashSet去重、LinkedHashSet去重、stream流单线程去重、stream流多线程去重。这里涉及到的数据结构先回顾一下:
ArrayList(有序不唯一):Object[],线程不安全,插入删除慢(数组移动),查询快(随机访问)。
HashSet(无序唯一):哈希表,基于HashMap实现,hashmap的key存储它的值。线程不安全。
LinkedHashSet(有序唯一):哈希表+链表(链表存储插入顺序),它是HashSet子类,内部只有构造函数。线程不安全。
// data
List<Integer> arrayList = new ArrayList<Integer>(){{add(3);add(2);add(4);add(2);add(3);add(5);add(4);add(5);}};
// HashSet去重
ArrayList<Integer> hashSetList = new ArrayList<>(new HashSet<>(arrayList));
// LinkedHashSet去重
ArrayList<Integer> linkedHashSetList = new ArrayList<>(new LinkedHashSet<>(arrayList));
打上断点查看一下数据
List<Integer> arrayList = new ArrayList<Integer>(){{add(3);add(2);add(4);add(2);add(3);add(5);add(4);add(5);}};
// stream去重
List<Integer> streamList = arrayList.stream().distinct().collect(Collectors.toList());
// parallelStream去重
List<Integer> parallelStreamList = arrayList.parallelStream().distinct().collect(Collectors.toList());
打上断点查看数据
上面就是简单数据的去重操作方法。
使用 Oracle 官方提供的性能测试工具 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)来测试一下这 4种 去重方式的性能。
首先,我们先要引入 JMH 框架,在 pom.xml
文件中添加如下配置:
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
<scope>provided</scope>
</dependency>
版本号我以及写好了,推荐使用最新版本。
下面编写测试代码,如下
@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)//测试 5 轮,每次 1s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class ArrayListDistinctTest {
static List<Integer> arrayList = new ArrayList<Integer>(){{
// 一共 2000000 条数据
for (int i = 0; i < 1000000; i++) add(i);
for (int i = 0; i < 1000000; i++) add(i);
}};
public static void main(String[] args) throws RunnerException {
// 启动基准测试
Options opt = new OptionsBuilder()
.include(ArrayListDistinctTest.class.getSimpleName()) // 要导入的测试类
.output("C:/Users/86177/Desktop/jmh-map.log") // 输出测试结果的文件
.build();
new Runner(opt).run(); // 执行测试
}
@Benchmark
public void hashSet(){
new ArrayList<>(new HashSet<>(arrayList));
}
@Benchmark
public void linkedHashSet(){
new ArrayList<>(new LinkedHashSet<>(arrayList));
}
@Benchmark
public void stream(){
arrayList.stream().distinct().collect(Collectors.toList());
}
@Benchmark
public void parallelStream(){
arrayList.parallelStream().distinct().collect(Collectors.toList());
}
}
所有被添加了 @Benchmark
注解的方法都会被测试,执行main方法即可。而正如代码中所示,我测试了一共 2000000(两百万) 条数据,有一半是重复的。已经把测试文件打印到桌面,我这里就贴上性能对比那一块数据:
Benchmark Mode Cnt Score Error Units
ArrayListDistinctTest.hashSet avgt 5 37251270.300 ± 10458771.326 ns/op
ArrayListDistinctTest.linkedHashSet avgt 5 40382889.634 ± 11785155.916 ns/op
ArrayListDistinctTest.parallelStream avgt 5 157258934.286 ± 34670245.903 ns/op
ArrayListDistinctTest.stream avgt 5 38513055.914 ± 3839780.617 ns/op
其中 Units 为 ns/op 意思是执行完成时间(单位为纳秒),而 Score 列为平均执行时间, ±
符号表示误差。从以上结果可以看出,两个 Set
的性能相近,并且执行速度很快,接下来是 stream
。
注:以上结果基于测试环境:JDK 1.8 / Win10 16GB (2021H) / Idea 2022.3
结论
除了以上数据量比较大的情况外,我还测试了进1000的小数据量。HashSet和LinkedHashSet的性能还是非常让人满意的,而对于stream来说在数据量比较小的情况下性能并没有Set高(parallelStream性能不如stream,下面有简单分析)。建议使用HashSet和LinkedHashSet去重。
基于以上,有ArrayList去重操作的时候,推荐使用LinkedHashSet。
去重操作说到底但还是基于集合的数据来说的,你看Set集合都不允许重复,当然是首先选它了。然后就是看一下需求,如HashSet去重后数据无序,LinkedHashSet去重后数据有序但是性能没有HashSet好。
Stream流选择的话有单线程的和多线程的流操作,这里的多线程操作流性能是要优于单线程操作的,需要判断一下当前操作有没有可能发生并发风险等。
注意一点:在计算机体系中,多线程操作未必一定优于多线程,单线程可能会优于多线程的原因通常是CPU切换操作线程造成的性能浪费,这当中会有一个线程休眠和线程唤醒操作。比如常用的Redis就设计成单线程。
我在网上找不到关于 here 的任何信息,谁能告诉我 ops/us, Cnt, Score, Error 是什么意思。 最佳答案 ops/us - 每微秒的操作(基准方法执行) Cnt - 试验总数(
我在网上找不到关于 here 的任何信息,谁能告诉我 ops/us, Cnt, Score, Error 是什么意思。 最佳答案 ops/us - 每微秒的操作(基准方法执行) Cnt - 试验总数(
在我的 jmh 课上,我正在使用 @BenchmarkMode(Mode.SampleTime) @Measurement(iterations = 10) @Threads(value = 10)
我已经将其视为微基准测试中的潜在陷阱之一。如果您指定@Measurement(或@Warmup)将运行固定的时间量,这意味着,当比较不同的运行(例如,不同的平台、不同版本的 VM 等)时,您将获得更少
想开始做我从现在开始写的方法的基准测试,有很长一段时间的动力,终于决定从昨天开始这样做。但我对我的设置过程感到震惊。 我已经正确安装了 JMH 插件。 所有导入工作正常。 甚至我的 POM 也没有显示
我正在使用 JMH,但发现有些难以理解:我有一种方法用 @Benchmark 注释。我设置了 measurementIterations(3) .该方法被调用了 3 次,但在每次迭代调用中,该函数运行
假设我有一个带有两个参数的 JMH 测试: @Param( { "1", "2", 4", "8", "16" } ) int param1; @Param( { "1", "2", 4", "8",
我正在使用tutorial学习JMH基准测试。 我注意到here中的功能benchMurmur3_128有2个与预热相关的东西。 因此,我对Fork注释中的热身属性和带有迭代属性的Warmup注释之间
我不明白 JMH 结果中的 score 属性?我也没有在网上找到任何关于它的信息。 谁能告诉我,它是关于什么的?据我所知,高分比低分好,但这究竟是什么意思,它是如何计算的? 最佳答案 JMH 支持以下
官方资源 官方Github样例 应用场景 对要使用的数据结构不确定,不知道谁的性能更好 对历史方法代码重构,要评判改造之后的性能提升多少 ( 我要做的场景 )
我用 JMH 测试我的程序性能。并且无法配置堆大小。我想知道为什么它不起作用。 问题: 为什么 JMH 不接受堆大小配置? JMH 是否在没有 jvmArgs 方法的情况下吸收 idea 堆大小设置?
我正在使用 JMH 对 DOM 解析器进行基准测试。我得到了非常奇怪的结果,因为第一次迭代实际上比后面的迭代运行得更快 谁能解释为什么会发生这种情况?另外,百分位数和所有数字是什么意思,为什么它在第三
我正在对 Spring Boot 应用程序启动时间进行基准测试。完整的项目是here ,这是 WIP,但相关类如下。 抽象基本状态: public abstract class BootAbstrac
我有:这样的方法: @GenerateMicroBenchmark public static void calculateArraySummary(String[] args) { // c
我读到了JMH并尝试了提供的示例。 我想做的是测量以下场景的统计数据, [ 1] client order -> [2] server -> [3] start processing the orde
我正在玩 Math.max 看看它是否受到分支预测的影响(不,至少在 x64 的 JDK 上不是,有一个 cmovl),如果按位实现可以与默认实现竞争。所有测试如下所示: @Threads(4) @S
我在 JMH 中看到一个常见问题 ConstantFold ,但是如果我有逆问题怎么办?我需要静态最终字段作为参数。例如,它可以是某些算法的某个常量变量。但在 java-doc 中我看到: {@lin
我正在尝试一个非常快的方法(~20 us/op),它似乎工作得很好,除了一些随机很长的迭代: Iteration 63: 14.319 us/op Iteration 64: 13.128 us/
我想看看是否有一种方法可以告诉 JMH 仅测量微基准调用的所有方法中的一个特定方法。 我想使用单元测试作为基础自动创建微基准,因此我不必手动构建微基准。在我的研究中,我有一个大型代码库,我在其中进行了
我正在运行 JMH 基准测试: Options opt = new OptionsBuilder() .output("C:/test/infinis
我是一名优秀的程序员,十分优秀!