【019】⁉️问: Java并发编程中Future和Callable有什么优点?


在Java并发编程中使用FutureCallable的好处:

1、异步执行: Callable代表一个有返回值的任务,Future可以用来获取这个任务的结果,实现异步执行。

2、获取结果: Future.get()方法用来获取Callable任务的执行结果。

3、异常处理: Future.get()会抛出执行中的异常,方便异常处理。

4、超时控制: Future.get()方法提供了超时控制的机制,防止无限等待。


📢 统计数据错到扣绩效?Future+Callable 教你给线程 “装个嘴”!

请添加图片描述
你刚用 Runnable 赶完公司 “季度销售复盘” 统计功能,拍着胸脯跟领导保证:“多线程并行算,1 分钟出结果,数据比财务的算盘还准”。结果复盘会现场直接翻车 —— 系统报的 “总销售额 120 万”,财务 Excel 里明明是 180 万,差额 60 万像凭空消失了。领导的脸当场黑成 IDE 的深色模式,散会后把你堵在工位:“下班前查不出问题,这个季度绩效直接打 C!”

你扒着代码头皮发麻:用 Runnable 写的三个统计线程(线上、线下、分销),算完数据全靠全局变量传值,线上线程抛了空指针异常还被悄悄吞了,数据根本没存进去;

更坑的是,主线程靠 Thread.sleep (60000) 瞎等,早一秒晚一秒都出问题。就在你把 “Runnable” 关键词标红想删代码时,隔壁工位叼着肉夹馍的王哥凑过来:“慌啥?Runnable 是线程界的‘哑巴员工’,干活不汇报,换 Callable 啊!再配个 Future 当‘取件码’,结果、异常全给你明明白白,比你这瞎猜靠谱 10 倍!”

一、先骂醒你:Runnable 是 “只会干活的哑巴”,坑全在暗处

王哥啃着肉夹馍,指着你写的 Runnable 代码笑出了双下巴:“你这代码跟雇了个‘闷葫芦’员工一样 —— 让他算线上订单,他算完揣兜里不吭声;算错了摔了计算器(抛异常),他也藏着掖着,你到最后只能对着‘0’汇总,不扣你绩效扣谁的?”

请添加图片描述

💯 你的 “翻车代码”(Runnable 哑巴版)

package cn.tcmeta.threads;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static java.lang.Thread.sleep;

/**
 * @author: laoren
 * @description: Runnable 哑巴版
 * @version: 1.0.0
 */
public class BadSalesStatSample {
    // 坑1:用全局变量传结果,多线程抢着改,线程不安全
    static int onlineSales = 0;    // 线上销售额
    static int offlineSales = 0;   // 线下销售额
    static int distributionSales = 0; // 分销销售额

    static void main() throws InterruptedException {
        // 线程池开3个线程,分别算三类数据
        ExecutorService pool = Executors.newFixedThreadPool(3);

        // 线程1:算线上销售额(实际该60万)
        pool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    sleep(2000); // 模拟查库耗时
                    // 这里抛了异常,你根本接不到!(坑2:Runnable吞异常)
                    int error = 1 / 0;
                    onlineSales = 600000;
                } catch (InterruptedException e) {
                    // 只抓了中断异常,业务异常直接吞了
                    e.printStackTrace();
                }
            }
        });

        // 线程2:算线下销售额(实际该80万)
        pool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    sleep(3000);
                    offlineSales = 800000;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 线程3:算分销销售额(实际该40万)
        pool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    sleep(2500);
                    distributionSales = 400000;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 坑3:瞎等!睡5秒赌线程算完,早了晚了都出问题
        sleep(5000);
        pool.shutdown();

        // 汇总结果(实际该180万,这里只算到120万)
        int total = onlineSales + offlineSales + distributionSales;
        System.out.println("季度总销售额:" + total);
    }
}

在这里插入图片描述
跑起来必输出季度总销售额:1200000—— 线上线程的 60 万因为异常丢了,你却毫不知情。王哥总结 Runnable 的三大 “致命坑”:

  • 无返回值:干活像 “黑箱”,结果全靠全局变量 / 共享对象传,线程安全问题全靠赌;
  • 吞异常:call () 方法不能抛 checked 异常,业务异常要么吞了要么靠 RuntimeException “暗箱操作”,排错像大海捞针;
  • 等待失控:主线程不知道线程啥时候跑完,只能靠 sleep () 瞎等,效率低还容易出错。

插个冷笑话:“Runnable 就像外卖员送外卖只送空袋子,你问‘我的炸鸡呢?’他只会摆手;Future+Callable 是正常外卖员,不仅送炸鸡,还会告诉你‘刚洒了点酱(抛异常)’,甚至会提前说‘还有 5 分钟到(状态查询)’—— 这服务差距,比你和架构师的工资差还大!”

二、Callable:给线程 “装个嘴”,干活会汇报结果和问题

请添加图片描述

王哥把肉夹馍油纸扔垃圾桶,打开 IDE 敲了几行代码:“Callable 本质是‘能说话的 Runnable’,核心就改了一点 —— 把 run () 方法换成 call (),既能返回结果,又能抛异常,线程从‘闷葫芦’变‘大喇叭’。”

Callable 的核心升级(对比 Runnable):

在这里插入图片描述
“你看这代码,线上订单线程算完直接返回 60 万,出问题直接抛异常,再也不用藏着掖着。” 王哥边说边写了两个 Callable 任务。

2.1 第一步:用 Callable 写 “会说话的统计任务”

请添加图片描述

package cn.tcmeta.threads;

import java.util.concurrent.Callable;

/**
 * @author: laoren
 * @description: TODO
 * @version: 1.0.0
 */
public class BadGoodsStatSample {
    // 线上订单统计任务:返回Integer类型结果(销售额)
    static class OnlineSalesTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("线上统计线程:开始查库算销售额...");
            Thread.sleep(2000); // 模拟数据库查询耗时

            // 模拟业务异常:比如数据库连接超时
            if (Math.random() > 0.3) { // 70%概率抛异常,方便测试
                throw new Exception("线上订单库连接超时,重试后恢复");
            }

            // 正常返回结果(60万)
            return 600000;
        }
    }

    // 线下订单统计任务
    static class OfflineSalesTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("线下统计线程:开始算门店销售额...");
            Thread.sleep(3000);
            return 800000; // 线下80万
        }
    }

    // 分销订单统计任务
    static class DistributionSalesTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("分销统计线程:开始算渠道销售额...");
            Thread.sleep(2500);
            return 400000; // 分销40万
        }
    }

    static void main() {
        BadGoodsStatSample sample = new BadGoodsStatSample();
        // 创建三个任务实例
        OnlineSalesTask onlineTask = new OnlineSalesTask();
        OfflineSalesTask offlineTask = new OfflineSalesTask();
        DistributionSalesTask distributionTask = new DistributionSalesTask();

        try {
            // 执行任务并获取结果
            Integer onlineResult = onlineTask.call();
            System.out.println("线上销售额: " + onlineResult);

            Integer offlineResult = offlineTask.call();
            System.out.println("线下销售额: " + offlineResult);

            Integer distributionResult = distributionTask.call();
            System.out.println("分销销售额: " + distributionResult);

            // 计算总销售额
            int totalSales = onlineResult + offlineResult + distributionResult;
            System.out.println("总销售额: " + totalSales);
        } catch (Exception e) {
            System.err.println("执行任务时发生异常: " + e.getMessage());
        }
    }
}

在这里插入图片描述
“现在线程会‘说话’了,但问题来了 —— 你怎么‘接话’?总不能趴在线程旁边等它喊吧?” 王哥卖了个关子,“这时候就得用 Future 当‘取件码’,线程算完结果存起来,你凭码随时取,还能查‘快递进度’。”

三、Future:线程结果的 “智能取件码”,等、取、催、取消全搞定

请添加图片描述

王哥解释:“你把 Callable 任务提交给线程池时,会拿到一个 Future 对象 —— 这就是‘取件码’。它能帮你干四件事:等结果、拿结果、查状态、取消任务,比你盯着线程池日志省心 10 倍。”

➡️ Future 的 5 个核心方法(人话版拆解)

在这里插入图片描述

3.1 第二步:Future+Callable 组合(完美解决统计翻车问题)

请添加图片描述

package cn.tcmeta.threads;

import java.util.concurrent.*;

/**
 * @author: laoren
 * @description: Future+Callable 组合(完美解决统计翻车问题)
 * @version: 1.0.0
 */

// 承接上面的三个Callable任务
public class GoodSalesStatSample {

    private static final int ONLINE_TIMEOUT_SECONDS = 3;
    private static final int OFFLINE_TIMEOUT_SECONDS = 4;
    private static final int DISTRIBUTION_TIMEOUT_SECONDS = 3;

    public static void main(String[] args) {
        // 1. 开3个线程的线程池,刚好对应三个统计任务
        ExecutorService pool = Executors.newFixedThreadPool(3);
        int totalSales = 0;

        // 2. 提交任务,拿到三个“取件码”
        Future<Integer> onlineFuture = pool.submit(new BadGoodsStatSample.OnlineSalesTask());
        Future<Integer> offlineFuture = pool.submit(new BadGoodsStatSample.OfflineSalesTask());
        Future<Integer> distributionFuture = pool.submit(new BadGoodsStatSample.DistributionSalesTask());

        try {
            // 3. 凭码拿线上结果:最多等3秒,超时就报错
            Integer online = onlineFuture.get(ONLINE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            if (online != null) {
                totalSales += online;
            }
            System.out.println("线上销售额:" + online);

            // 4. 拿线下结果:最多等4秒
            Integer offline = offlineFuture.get(OFFLINE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            if (offline != null) {
                totalSales += offline;
            }
            System.out.println("线下销售额:" + offline);

            // 5. 拿分销结果:最多等3秒
            Integer distribution = distributionFuture.get(DISTRIBUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            if (distribution != null) {
                totalSales += distribution;
            }
            System.out.println("分销销售额:" + distribution);

            // 6. 汇总结果(这次准了!)
            System.out.println("=====================");
            System.out.println("季度总销售额:" + totalSales);
            System.out.println("=====================");

        } catch (InterruptedException e) {
            System.out.println("统计线程被意外中断");
        } catch (ExecutionException e) {
            // 关键:捕获Callable抛的业务异常,知道哪里错了
            System.out.println("统计出错:" + e.getCause().getMessage());
        } catch (TimeoutException e) {
            // 超时处理:取消所有任务,避免浪费资源
            onlineFuture.cancel(true);
            offlineFuture.cancel(true);
            distributionFuture.cancel(true);
            System.out.println("统计超时,已取消所有任务");
        } finally {
            // 关闭线程池,避免资源泄漏
            pool.shutdown();
            try {
                if (!pool.awaitTermination(1, TimeUnit.SECONDS)) {
                    pool.shutdownNow();
                }
            } catch (InterruptedException ie) {
                pool.shutdownNow();
            }
        }
    }
}

在这里插入图片描述

3.2 两种运行结果,都比 Runnable 靠谱:

1. 正常情况(30% 概率):

plaintext
线上统计线程:开始查库算销售额...
线下统计线程:开始算门店销售额...
分销统计线程:开始算渠道销售额...
线上销售额:600000
线下销售额:800000
分销销售额:400000
=====================
季度总销售额:1800000
=====================

2. 线上线程抛异常(70% 概率):

线上统计线程:开始查库算销售额...
线下统计线程:开始算门店销售额...
分销统计线程:开始算渠道销售额...
统计出错:线上订单库连接超时,重试后恢复

“你看,现在结果准了,异常也能抓到,再也不用背‘数据消失’的锅了!” 王哥拍桌,“这就是 Future+Callable 的核心价值 —— 把线程从‘只管干活’的工具人,变成‘干活 + 汇报’的得力助手。”

四、实战场景:这 3 种情况,必须用 Future+Callable

请添加图片描述

王哥喝了口冰可乐,给你列了 “不用就翻车” 的三个场景,每个都配了极简代码模板,看完就能抄。

4.1 场景 1:多任务并行计算(最常用)

比如统计 “订单、库存、用户” 三个维度数据,用 3 个 Callable 并行跑,比串行快 3 倍。

// 模板:多任务并行汇总
public class ParallelTaskDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newCachedThreadPool();
        // 三个任务并行
        Future<Integer> orderFuture = pool.submit(() -> 1000); // 订单数
        Future<Long> stockFuture = pool.submit(() -> 5000L);  // 库存数
        Future<String> userFuture = pool.submit(() -> "10万");// 用户数

        // 拿结果汇总
        System.out.println("订单数:" + orderFuture.get());
        System.out.println("库存数:" + stockFuture.get());
        System.out.println("用户数:" + userFuture.get());
        pool.shutdown();
    }
}

4.2 场景 2:异步调用第三方接口(避免页面卡顿)

调用支付、物流等第三方接口时,接口可能卡 3 秒,用 Callable 异步调用,主线程继续干别的。

// 模板:异步调用第三方接口
public class AsyncApiDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newSingleThreadExecutor();

        // 异步调用支付接口(主线程不用等)
        Future<String> payFuture = pool.submit(() -> {
            // 调用支付宝/微信支付接口
            Thread.sleep(3000); // 模拟接口耗时
            return "支付成功,订单号:20240618001";
        });

        // 主线程继续干别的(比如更新订单状态为“支付中”)
        System.out.println("主线程:更新订单状态为支付中");

        // 等需要支付结果时再拿
        String payResult = payFuture.get(5, TimeUnit.SECONDS);
        System.out.println("主线程:" + payResult);
        pool.shutdown();
    }
}

3.3 场景 3:可取消的耗时任务(比如生成大报表)

用户生成 10 万条数据的报表,等了 5 秒不耐烦关掉页面,用 Future.cancel () 直接终止任务,省 CPU。

// 模板:可取消的耗时任务
public class CancelTaskDemo {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newSingleThreadExecutor();

        // 生成大报表的任务
        Future<String> reportFuture = pool.submit(() -> {
            for (int i = 0; i < 100; i++) {
                // 检查任务是否被取消,被取消就退出
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("报表生成任务已取消");
                    return "任务取消";
                }
                Thread.sleep(100); // 模拟生成进度
            }
            return "报表生成完成:report_20240618.xlsx";
        });

        // 模拟用户5秒后取消任务
        Thread.sleep(5000);
        reportFuture.cancel(true); // 取消任务,中断正在执行的线程

        pool.shutdown();
    }
}

总结一波:“Future+Callable 就像公司的‘靠谱员工’:

  • 你安排他做报表(submit (Callable)),他会告诉你‘我开始做了’(启动线程);
  • 做不完你问他‘啥时候好’(isDone ()),他会说‘还剩 30%’(状态反馈);
  • 做完了主动把报表放你桌上(get () 拿结果);
  • 做砸了会跟你说‘数据库崩了,需要重试’(抛异常);
  • 你说‘不用做了’(cancel ()),他会立刻停手,不会瞎忙活 —— 这才是打工人的自我修养!”

五、进阶:FutureTask—— 能当 Runnable 的 “全能选手”

请添加图片描述
“还有个更灵活的工具叫 FutureTask,” 王哥突然说,“它实现了 Runnable 和 Future 两个接口,既能当 Runnable 传给 Thread,又能当 Future 拿结果,相当于‘线程界的瑞士军刀’。”

FutureTask 实战(单线程场景超好用)
比如你只需要一个线程算数据,不用开线程池,FutureTask 更轻便:

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class FutureTaskDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1. 用Callable包装任务,生成FutureTask
        FutureTask<Integer> salesTask = new FutureTask<>(() -> {
            System.out.println("统计线程:开始计算...");
            Thread.sleep(2000);
            return 1800000; // 总销售额
        });

        // 2. 既能传给Thread(因为实现了Runnable)
        new Thread(salesTask).start();

        // 3. 又能拿结果(因为实现了Future)
        Integer total = salesTask.get(3, TimeUnit.SECONDS);
        System.out.println("总销售额:" + total);
    }
}

销售额,用 FutureTask 比‘线程池 + Callable’简洁多了。”

六、总结:Future+Callable 的核心优点(记牢不踩坑)

王哥把代码保存好,给你画了个 “傻瓜式总结表”,贴显示器上比便利贴管用:
Callable 的 3 个核心优点:

  • 结果可见:泛型指定返回值,算完直接 return,不用全局变量瞎传,线程安全;
  • 异常可控:call () 能抛任意异常,上层用 ExecutionException 接住,排错不用猜;
  • 类型安全:编译期就检查返回值类型,不会出现 “把 String 当 Integer” 的低级错误。

Future 的 4 个核心优点

  • 精准取结果:get () 方法按需拿结果,不用轮询日志;
  • 超时保护:get (超时时间) 避免线程死等,线上环境必加;
  • 任务可控:cancel () 能终止无用任务,省 CPU 省内存;
  • 状态透明:isDone ()/isCancelled () 随时查进度,灵活处理各种场景。

彩蛋:王哥的血泪史
“我刚用 Future 的时候,把 get () 写在循环里轮询,” 王哥捂脸,“就像每隔 100 毫秒问一次‘结果好了吗’,结果线程池被我堵成‘停车场入口’,CPU 飙到 90%。领导以为服务器被黑客攻击了,最后发现是我代码写蠢了 —— 罚我抄了 10 遍 Future 的 API 文档!”

七、最后说句实在的

请添加图片描述

Future 和 Callable 不是 “高大上技术”,而是解决 Runnable 痛点的 “刚需工具”—— 简单场景(比如只打印日志,不用结果)用 Runnable 凑活;只要涉及 “要结果、要抓异常、要控制任务”,果断用 Future+Callable。

今天这几版代码你复制过去就能跑,改改返回值类型和业务逻辑,就能用到统计、接口调用、报表生成等场景,再也不用背 “数据错漏” 的锅。

要是你搞懂了,别光顾着自己爽,点赞让更多人避坑,关注我 —— 下次咱们扒一扒 .

对了,把这篇分享给你那还用 “Runnable + 全局变量” 传值的同事,下次代码 review 时,你就能笑着说:“兄弟,别让线程当哑巴了,给它装个嘴(Callable),再配个智能取件码(Future),干活汇报两不误!”

请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值