在Java并发编程中,我们经常需要对共享资源进行原子性操作,比如计数。Java的
java.util.concurrent.atomic
包提供了一些原子类,如AtomicInteger
、AtomicLong
等,它们通过硬件级别的原子操作来保证线程安全。然而,在高并发的场景下,这些原子类的性能可能会成为瓶颈。为了解决这个问题,Java8在java.util.concurrent.atomic
包中引入了LongAdder
类。
目录
- 核心概述
- 一、LongAdder的使用
- 二、LongAdder的性能优势
- 三、LongAdder的实现原理
- 1. 分段锁思想
- 2. 并发控制
- 3. 变量合并与求和
- 四、总结
核心概述
LongAdder
是一个用于并发环境中的长整型加法操作的类,它提供了比AtomicLong
更高的吞吐量。LongAdder
在内部维护了一个或多个变量(取决于当前并发级别和系统环境),每个线程对其中一个变量进行操作,从而减少了线程间的竞争。当需要获取总和时,这些变量会被加在一起。
与AtomicLong相比,它通过内部维护多个Cell对象,采用分段化的方式降低线程间的并发冲突,从而提高了性能。然而,这种设计也带来了一定的内存开销。LongAdder常用于需要高并发更新的统计和计数场景。
一、LongAdder的使用
下面代码展示了如何在多线程环境中使用LongAdder
来统计并发任务的执行次数,并最终获取总的执行次数。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.LongAdder;
public class LongAdderComplexExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个LongAdder用于统计任务执行次数
LongAdder taskCounter = new LongAdder();
// 创建一个线程池用于执行并发任务
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 创建一个任务列表
List<Runnable> tasks = new ArrayList<>();
// 添加100个任务到任务列表
for (int i = 0; i < 100; i++) {
final int taskId = i;
tasks.add(() -> {
// 模拟任务执行时间
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 任务执行完毕,增加计数器
taskCounter.increment();
System.out.println("Task " + taskId + " completed.");
});
}
// 提交任务到线程池执行
for (Runnable task : tasks) {
executorService.submit(task);
}
// 关闭线程池(这会导致正在执行的任务完成后,线程池不再接受新任务)
executorService.shutdown();
// 等待所有任务执行完毕
while (!executorService.isTerminated()) {
// 等待线程池终止
}
// 输出任务执行总次数
System.out.println("Total tasks completed: " + taskCounter.sum());
}
}
首先创建了一个LongAdder
对象taskCounter
用于统计任务执行次数。然后,我们创建了一个固定大小的线程池executorService
,用于并发执行任务。
接下来,我们创建了一个包含100个任务的列表tasks
。每个任务都是一个Runnable
对象,在其run
方法中,我们模拟了任务执行的时间(通过Thread.sleep
方法),并在任务执行完毕后使用LongAdder
的increment
方法增加计数器。
然后,我们将这些任务提交到线程池执行,并关闭线程池以拒绝新任务的提交。我们使用executorService.isTerminated()
方法检查线程池是否已终止(即所有任务都已执行完毕),并在所有任务执行完毕后输出任务执行的总次数(通过LongAdder
的sum
方法获取)。
需要注意的是,在实际应用中,我们可能需要更精细地控制任务的提交和执行过程,例如使用CountDownLatch
、CyclicBarrier
或Semaphore
等并发工具类来协调多个线程的执行顺序或限制并发数。此外,对于需要长时间运行的任务或需要频繁更新计数器的场景,我们可以考虑使用其他的并发容器或数据结构来优化性能。
二、LongAdder的性能优势
与AtomicLong
相比,LongAdder
在高并发场景下的性能优势主要体现在以下几个方面:
- 减少线程间的竞争:
LongAdder
内部维护了多个变量,每个线程对其中一个变量进行操作,从而减少了线程间的竞争。这使得在高并发场景下,LongAdder
的性能优于AtomicLong
。 - 适用于统计和计数场景:
LongAdder
适用于统计和计数场景,如记录某个方法的调用次数、统计某个事件的发生次数等。在这些场景中,我们不需要关心中间状态,只需要获取最终的总和。
然而,需要注意的是,LongAdder
并不适用于所有场景。在需要精确控制中间状态的场景中(如需要获取任意时刻的精确值),AtomicLong
可能更合适。此外,LongAdder
的sum
方法可能会比AtomicLong
的get
方法更耗时,因为它需要遍历内部的所有变量并求和。
三、LongAdder的实现原理
LongAdder
的实现原理是基于分段锁和并发控制的思想,通过内部维护多个变量来减少线程间的竞争,从而提高并发性能。下面我们将深入分析LongAdder
的实现原理。
1. 分段锁思想
LongAdder
内部维护了一个或多个Cell
对象,每个Cell
对象包含一个长整型变量。这些Cell
对象构成了一个数组,数组的大小通常是2的幂次方,以便使用位运算快速定位。每个线程在对LongAdder
进行操作时,会根据当前线程的哈希码通过特定的哈希算法选择一个Cell
对象进行操作。这种分段锁的思想类似于ConcurrentHashMap中的分段锁机制,通过将数据分散到多个段(Cell
)上,减少了线程间的竞争。
2. 并发控制
当线程对LongAdder
进行操作时,它会首先尝试获取对应Cell
对象的锁(通过CAS操作实现)。如果成功获取锁,则线程会安全地更新该Cell
对象的值。如果失败,则线程会尝试获取其他Cell
对象的锁,或者更新base
变量。这种并发控制机制确保了在高并发场景下,多个线程可以同时进行加法操作,而不会相互阻塞。
需要注意的是,LongAdder
并不保证每个线程都固定地操作同一个Cell
对象。当线程竞争同一个Cell
对象失败时,它会尝试获取其他Cell
对象的锁。这种灵活性使得LongAdder
能够更好地适应动态变化的并发环境。
3. 变量合并与求和
当需要获取LongAdder
的总和时,会遍历内部的所有Cell
对象并将它们的值累加起来,然后再加上base
变量的值。这个过程可能需要花费一些时间,因为需要遍历整个Cell
数组。然而,在实际应用中,我们通常不需要频繁地获取总和,而是更关注于并发性能的优化。
需要指出的是,虽然LongAdder
提供了比AtomicLong
更高的吞吐量,但它并不适用于所有场景。在需要精确控制中间状态的场景中(如需要获取任意时刻的精确值),AtomicLong
可能更合适。此外,LongAdder
的sum
方法可能会比AtomicLong
的get
方法更耗时,因为它需要遍历内部的所有变量并求和。因此,在选择使用LongAdder
还是AtomicLong
时,需要根据实际需求进行权衡和选择。
总之,LongAdder
通过分段锁和并发控制的思想实现了高并发场景下的长整型加法操作优化。它内部维护了多个变量来减少线程间的竞争,并提供了灵活的并发控制机制以适应动态变化的并发环境。然而,在使用LongAdder
时需要注意其适用场景和限制,并根据实际需求选择合适的并发工具类。
四、总结
LongAdder
是Java并发库中的一个非常有用的工具类,它提供了比AtomicLong
更高的吞吐量,适用于高并发场景下的统计和计数操作。然而,在使用LongAdder
时,我们需要注意其适用场景和限制,并根据实际需求选择合适的并发工具类。