
在java并发编程中,java.util.concurrent.future接口代表一个异步计算的结果。它允许我们检查计算是否完成、等待计算完成以及获取计算结果。然而,将list<future<integer>>用于存储一组需要频繁修改的整数值,并尝试直接修改其内部值,是一个常见的误区。
原始代码片段中,elements被声明为List<Future<Integer>>:
List<Future<Integer>> elements = new ArrayList<>(); // ... elements.set(firstIndex, elements.get(firstIndex).get() - randomAmount);
这里存在两个核心问题:
此外,初始代码在填充elements列表时,即使是简单的初始值1000,也通过ex.submit(() -> { int val = 1000; return val; })来获取Future。这种做法对于静态初始值而言是过度设计,且没有必要。如果只是为了存储初始值,直接使用List<Integer>并添加整数即可。
另一个关键问题是ExecutorService的生命周期管理。在原始代码中,首次提交完100个初始化任务后,立即调用了ex.shutdown():
立即学习“Java免费学习笔记(深入)”;
// ...
for (int i = 0; i < 100; i++) {
elements.add(ex.submit(() -> { /* ... */ }));
}
ex.shutdown(); // 过早关闭
// ...
for (int i = 0; i < 10_000; i++) {
ex.submit(() -> { /* ... */ }); // 此处会因ExecutorService已关闭而失败
}ExecutorService.shutdown()方法会平缓地关闭线程池,不再接受新的任务提交,但会等待已提交任务执行完成。如果在此之后尝试提交新任务(如后续的10,000个转账任务),将会抛出RejectedExecutionException。正确的做法是,只有在所有任务都已提交且不再需要线程池时,才调用shutdown()。对于本例,shutdown()应该在所有转账任务提交完毕之后再调用。
鉴于上述问题,如果目标是存储和修改一组整数值,并允许多线程并发访问,我们需要选择一个合适的并发数据结构。直接使用ArrayList<Integer>虽然解决了类型问题,但在多线程环境下对共享的ArrayList进行读写操作会引发竞态条件,导致数据不一致。
java.util.concurrent.atomic.AtomicIntegerArray是Java并发包提供的一个高效且线程安全的数组。它内部的每个元素都是一个AtomicInteger,支持原子性的读取、写入、更新等操作,无需显式使用synchronized关键字或锁。这非常适合本例中对数组中特定索引位置的整数进行并发修改的需求。
AtomicIntegerArray的优势:
以下是修正后的代码,它使用AtomicIntegerArray来存储和修改元素,并正确管理了ExecutorService的生命周期:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class ConcurrentMoneyTransfer {
public static void main(String[] args) throws InterruptedException {
// 1. 初始化 ExecutorService
// 使用固定大小的线程池,例如10个线程
ExecutorService ex = Executors.newFixedThreadPool(10);
// 2. 使用 AtomicIntegerArray 存储可变整数值
// 初始包含100个元素,每个元素值为1000
AtomicIntegerArray elements = new AtomicIntegerArray(100);
for (int i = 0; i < 100; i++) {
elements.set(i, 1000);
}
// 计算初始总和
int initialSum = 0;
for (int i = 0; i < elements.length(); i++) {
initialSum += elements.get(i);
}
System.out.println("Initial sum: " + initialSum); // 预期输出 100 * 1000 = 100000
// 3. 提交并发转账任务
// 模拟10,000次转账操作
for (int i = 0; i < 10_000; i++) {
ex.submit(() -> {
int firstIndex = ThreadLocalRandom.current().nextInt(100);
int secondIndex = ThreadLocalRandom.current().nextInt(100); // 尽管未使用,保留原意
int randomAmount = ThreadLocalRandom.current().nextInt(1000);
// 原子性地获取并检查余额
int currentFirstValue = elements.get(firstIndex);
if (currentFirstValue - randomAmount >= 0) { // 确保余额足够
// 原子性地减少第一个账户的金额
elements.getAndAdd(firstIndex, -randomAmount);
// 如果有转入操作,也需要原子性增加
// elements.getAndAdd(secondIndex, randomAmount); // 如果需要转入,这里可以添加
}
});
}
// 4. 关闭 ExecutorService
// 等待所有提交的任务完成
ex.shutdown();
// 设置一个超时,防止无限等待
if (!ex.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("ExecutorService did not terminate in time.");
ex.shutdownNow(); // 强制关闭
}
// 5. 计算最终总和
int finalSum = 0;
for (int i = 0; i < elements.length(); i++) {
finalSum += elements.get(i);
}
System.out.println("Final sum: " + finalSum); // 预期输出小于等于 initialSum
}
}代码说明:
在Java并发编程中,理解Future对象的本质及其不可变性至关重要。将其误用作可变数据容器不仅会导致编译错误,还会混淆并发模型。对于需要多线程并发修改的共享数据,应选择AtomicIntegerArray等原子类或并发集合,它们提供了高效且线程安全的机制。同时,正确管理ExecutorService的生命周期,确保在所有任务提交和执行完成后再关闭线程池,是编写健壮并发程序的关键。通过遵循这些原则,可以有效地避免常见的并发陷阱,构建出高性能、高可靠性的并发应用。
以上就是Java并发编程:理解Future的不可变性与共享数据修改策略的详细内容,更多请关注php中文网其它相关文章!
编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号