0

0

Java并发编程:掌握Future、线程安全与原子操作

聖光之護

聖光之護

发布时间:2025-08-29 21:05:01

|

934人浏览过

|

来源于php中文网

原创

Java并发编程:掌握Future、线程安全与原子操作

本教程深入探讨在Java并发编程中,如何避免将Future对象错误地用于存储可变数据,并详细指导如何正确地管理ExecutorService生命周期以及利用AtomicIntegerArray等并发工具实现线程安全的共享数组元素更新,确保数据一致性。

1. 理解Future的本质与误用

java并发编程中,future接口代表一个异步计算的结果。当我们将一个任务提交给executorservice时,submit()方法会返回一个future对象,通过它可以查询任务是否完成、取消任务以及获取任务的最终结果。future的核心在于其表示的是一个未来可获得且一旦计算完成便不可变的结果

原始代码中尝试使用List>来存储和直接修改整数值:

List<Future<Integer>> elements = new ArrayList<>();
// ...
elements.set(firstIndex, elements.get(firstIndex).get() - randomAmount);

这里存在两个主要问题:

  1. 类型不匹配:elements.set()方法的第二个参数期望的是一个Future对象,但代码中传入的是一个int类型的值(elements.get(firstIndex).get() - randomAmount的结果)。这直接导致了编译错误:set> is not applicable to arguments (int,int)。
  2. Future的不可变性:即使类型匹配,Future对象一旦其内部结果被设定,其结果本身是不可变的。你不能直接“修改”一个Future所持有的值。如果需要修改,实际上是需要创建一个新的Future对象来替代旧的,但这通常不是管理可变共享数据的正确方式。

因此,Future不应被用作直接存储和修改可变共享数据的容器。当我们需要一个可变整数数组并在多个线程中对其进行操作时,应该选择更合适的并发数据结构。

2. 选择正确的共享数据结构

对于需要在多个线程间共享并进行修改的整数数组,我们应该避免使用Future列表。最直接的替代方案是使用List或int[]。然而,ArrayList和普通数组本身都不是线程安全的。当多个线程同时对它们进行读写操作时,可能会发生竞态条件,导致数据不一致。

立即学习Java免费学习笔记(深入)”;

为了确保线程安全,Java提供了多种并发工具。在本场景中,AtomicIntegerArray是管理整数数组并提供原子性操作的理想选择。

AtomicIntegerArray提供了对数组中每个元素的原子性操作,例如get()、set()、compareAndSet()、getAndAdd()等,这些操作能够保证在多线程环境下的数据一致性,避免了手动加锁的复杂性。

网梦购物系统
网梦购物系统

一套功能完善、性能稳定的经典网上购物系统,掌握了一整套从算法,数据结构到产品安全性方面的领先技术,使程序无论在安全性、负载能力方面均获得了成功,新版购物系统集成多种在线支付方式,全后台操作管理,并集成了Ewebedit编辑器,即使只有电脑基础知识的人也能够轻松操作和管理部分新增功能:集成多种网上支付形式,后台灵活切换增加Ewebedit编辑器,添加信息更容易!简洁、明快、新颖的界面,给人以美的感觉

下载

3. ExecutorService的生命周期管理

原始代码中,在提交第一批初始化任务后就调用了ex.shutdown():

// ... 初始化 elements 列表后
ex.shutdown(); // 过早关闭 ExecutorService
// ... 之后尝试提交新的更新任务

ExecutorService一旦调用了shutdown()方法,它将不再接受新的任务,但会完成所有已提交的任务。如果过早调用shutdown(),后续需要提交的更新任务将无法被执行,通常会抛出RejectedExecutionException。

正确的做法是,在所有任务都提交完毕并且我们希望等待它们全部执行完成后,才调用shutdown()。为了确保所有任务都已完成,我们通常会结合使用shutdown()和awaitTermination()方法。awaitTermination()会阻塞当前线程,直到所有任务执行完毕或者达到指定的超时时间。

4. 实现线程安全的数组更新

为了解决原始问题中的编译错误和潜在的并发问题,我们将采用AtomicIntegerArray并正确管理ExecutorService的生命周期。

示例代码:使用AtomicIntegerArray实现线程安全的数组更新

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.ThreadLocalRandom;

public class ConcurrentArrayUpdateTutorial {

    public static void main(String[] args) throws InterruptedException {
        // 1. 初始化 ExecutorService,用于管理并发任务
        var ex = Executors.newFixedThreadPool(10);

        // 2. 使用 AtomicIntegerArray 存储可变整数,提供线程安全
        // AtomicIntegerArray 内部管理一个 int[],并为每个元素提供原子操作
        AtomicIntegerArray elements = new AtomicIntegerArray(100);
        for (int i = 0; i < 100; i++) {
            elements.set(i, 1000); // 初始化每个元素为1000
        }

        // 计算初始总和
        int initialSum = 0;
        for (int i = 0; i < elements.length(); i++) {
            initialSum += elements.get(i);
        }
        System.out.println("Initial sum: " + initialSum);

        // 3. 提交大量并发更新任务
        int numberOfUpdates = 10_000;
        // 使用 CountDownLatch 等待所有更新任务完成
        CountDownLatch latch = new CountDownLatch(numberOfUpdates);

        for (int i = 0; i < numberOfUpdates; i++) {
            ex.submit(() -> {
                try {
                    int firstIndex = ThreadLocalRandom.current().nextInt(100);
                    // int secondIndex = ThreadLocalRandom.current().nextInt(100); // 原始问题中提及,但在逻辑中未被使用

                    int randomAmount = ThreadLocalRandom.current().nextInt(1000);

                    // 确保减法操作的原子性及条件判断
                    boolean updated = false;
                    while (!updated) {
                        int currentValue = elements.get(firstIndex); // 原子性获取当前值

                        // 检查是否有足够的量可以减去
                        if (currentValue >= randomAmount) {
                            // 使用 compareAndSet 尝试原子性地更新
                            // 如果 currentValue 在此期间被其他线程修改,compareAndSet 会失败,
                            // 循环会再次执行,获取新的 currentValue 并重试。
                            if (elements.compareAndSet(firstIndex, currentValue, currentValue - randomAmount)) {
                                updated = true; // 更新成功,退出循环
                                // 如果需要将 randomAmount 加到 secondIndex,可以在这里进行原子操作
                                // elements.getAndAdd(secondIndex, randomAmount);
                            }
                        } else {
                            // 当前值不足以减去 randomAmount,放弃本次更新
                            updated = true; // 退出循环
                        }
                    }
                } finally {
                    latch.countDown(); // 任务完成,计数器减一
                }
            });
        }

        // 4. 等待所有任务完成
        latch.await(); // 阻塞当前线程,直到所有任务都调用了 countDown()
        ex.shutdown(); // 提交所有任务后,关闭 ExecutorService
        // 等待 ExecutorService 中的所有任务真正终止
        if (!ex.awaitTermination(5, TimeUnit.SECONDS)) {
            System.err.println("Executor did not terminate in time. Forcing shutdown.");
            ex.shutdownNow(); // 如果超时,则强制关闭
        }

        // 5. 计算最终总和
        int finalSum = 0;
        for (int i = 0; i < elements.length(); i++) {
            finalSum += elements.get(i);
        }
        System.out.println("Final sum: " + finalSum);
    }
}

代码解析与注意事项:

  • AtomicIntegerArray:它取代了List>,直接存储整数,并提供了线程安全的get()和set()方法。更重要的是,对于条件性更新(如if (value - amount > 0)),我们使用了compareAndSet循环模式,这是一种确保原子性操作的经典方法。
    • elements.get(firstIndex):原子性地获取指定索引的值。
    • elements.compareAndSet(firstIndex, currentValue, currentValue - randomAmount):这是一个原子操作。它会检查firstIndex处的当前值是否仍然是currentValue。如果是,则将其原子性地更新为currentValue - randomAmount并返回true;否则(说明在get()和compareAndSet()之间,该值已被其他线程修改),它不执行更新并返回false。通过while循环,我们可以不断重试,直到更新成功或条件不满足。
  • ExecutorService生命周期:ex.shutdown()被移到了所有任务提交之后,latch.await()确保所有任务执行完毕。awaitTermination()则用于等待线程池优雅关闭。
  • CountDownLatch:用于主线程等待所有提交的子任务完成。每个任务完成时调用latch.countDown(),主线程通过latch.await()阻塞直到计数器归零。
  • ThreadLocalRandom:在多线程环境中生成随机数时,ThreadLocalRandom比Random更高效且避免了竞争。

5. 最佳实践与总结

  1. 区分Future与可变数据:Future用于获取异步计算的结果,不应用于存储和修改共享的可变数据。
  2. 选择合适的并发数据结构:当需要并发访问和修改共享数据时,优先考虑java.util.concurrent包中的并发集合(如ConcurrentHashMap、CopyOnWriteArrayList)或java.util.concurrent.atomic包中的原子类(如AtomicInteger、AtomicLong、AtomicReference、AtomicIntegerArray)。它们提供了比手动加锁更高效、更安全的并发控制机制。
  3. 正确管理ExecutorService生命周期:确保在所有任务提交完成后才调用shutdown(),并使用awaitTermination()等待任务完成,以避免资源泄露和任务丢失。
  4. 原子性操作:对于涉及读取、判断和写入的复合操作,简单的get()和set()组合不足以保证线程安全。应使用原子类提供的compareAndSet()或getAndAdd()等方法来确保整个操作的原子性。当需要条件性更新时,compareAndSet循环模式是常见的解决方案。
  5. 避免过早优化:在设计并发程序时,首先要保证正确性,然后才是性能。线程安全是并发编程的基石。

通过遵循这些原则,可以编写出更健壮、更高效的Java并发应用程序。

相关文章

编程速学教程(入门课程)
编程速学教程(入门课程)

编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

846

2023.08.22

while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

106

2023.09.25

while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

106

2023.09.25

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1010

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

610

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

334

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

235

2025.08.29

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

548

2023.12.01

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

3

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

相关下载

更多

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.1万人学习

Java 教程
Java 教程

共578课时 | 80.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号