Java多线程:Callable解析(附实战案例)

文章提示

适合人群:具备Java基础、了解线程基本概念
你将学会:

  1. 线程创建的四种方式

  2. Callable与Runnable的核心区别

  3. FutureTask的实战应用技巧

  4. 避免常见多线程陷阱的方法


目录

文章提示

前言

一、线程创建的四大门派

1. 继承Thread类(青铜段位)

2. 实现Runnable接口(白银段位)

 3. Callable+FutureTask(黄金组合)

 4. 线程池(王者之选) 

二、Runnable vs Callable 世纪对决

1. Callable 接口

2. FutureTask 类

 1.代码示例:

2.代码分析:

总结

三、必知必会的两大特性

1. 结果缓存机制

 2. 阻塞式获取结果

高频面试题精解

总结与升华


前言

在Java多线程开发中,Runnable和Callable就像两位性格迥异的双胞胎兄弟。他们都能执行异步任务,但一个沉默寡言(没有返回值),一个活泼外向(能带回结果)。今天我们将通过生活化的比喻和实战代码,带你彻底掌握这对兄弟的差异,以及他们的黄金搭档——FutureTask的使用秘籍!


一、线程创建的四大门派

1. 继承Thread类(青铜段位)

class MyThread extends Thread {
    public void run() {
        System.out.println("继承Thread方式");
    }
}
// 使用
new MyThread().start();

2. 实现Runnable接口(白银段位)

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("实现Runnable方式");
    }
}
// 使用
new Thread(new MyRunnable()).start();

 3. Callable+FutureTask(黄金组合)

class MyCallable implements Callable {
    public String call() throws Exception {
        return "Callable返回值";
    }
}
// 使用
FutureTask task = new FutureTask<>(new MyCallable());
new Thread(task).start();
System.out.println(task.get()); // 获取返回值

 4. 线程池(王者之选) 

ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(() -> System.out.println("线程池方式"));
pool.shutdown();

想要了解更多可以参考作者的: JUC多线程:一篇文章搞懂线程池:从核心参数到源码设计全解析-CSDN博客

二、Runnable vs Callable 世纪对决

在 Java 中,Callable 接口和 FutureTask 类提供了比 Runnable 更强大的功能,特别是支持返回值和异常处理 

Java多线程:Callable解析(附实战案例)_第1张图片

1. Callable 接口

定义:Callable 接口类似于 Runnable,但它可以返回一个结果,并且可以抛出受检异常
方法:Callable 接口只有一个方法 call(),该方法返回一个泛型类型的结果,并且可以抛出 Exception。

2. FutureTask 类

定义FutureTask 类实现了 Runnable 和 Future 接口,可以包装一个 Callable 对象,并提供异步执行任务的能力。
功能

  • 异步执行:可以将 Callable 任务提交给线程池或单独的线程执行。
  • 获取结果:通过 FutureTask 可以获取 Callable 任务的执行结果。
  • 缓存结果:一旦 Callable 任务执行完毕,结果会被缓存,后续调用 get() 方法不会重新执行任务。
 1.代码示例:
package JUC.Usafe;

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

/**
 * 1、探究原理
 * 2、觉自己会用
 */
public class CallableTest {
    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        // new Thread(new Runnable()).start();
        // new Thread(new FutureTask()).start();
        // new Thread(new FutureTask( Callable )).start();
        new Thread().start(); // 怎么启动Callable
        MyThread thread = new MyThread();
        FutureTask futureTask = new FutureTask(thread); // 适配类
        new Thread(futureTask,"A").start();
        new Thread(futureTask,"B").start(); // 结果会被缓存,效率高
        Integer o = (Integer) futureTask.get(); //这个get 方法可能会产生阻塞!把他放到最后
        // 或者使用异步通信来处理!
        System.out.println(o);
    }
}
class MyThread implements Callable {
    @Override
    public Integer call() {
        System.out.println("call()"); // 会打印几个call
        // 耗时的操作
        return 1024;
    }
}
2.代码分析:

CallableTest 类展示了如何使用 Callable 和 FutureTask 来执行任务并获取结果。以下是关键点分析:
MyThread 类实现 Callable 接口:

  class MyThread implements Callable {
      @Override
      public Integer call() {
          System.out.println("call()"); // 会打印几个call
          // 耗时的操作
          return 1024;
      }
  }
  

 MyThread 类实现了 Callable 接口,重写了 call() 方法。
        call() 方法打印 "call()" 并返回整数 1024。
CallableTest 类中的 main 方法

  public class CallableTest {
      public static void main(String[] args) throws ExecutionException, InterruptedException {
          MyThread thread = new MyThread();
          FutureTask futureTask = new FutureTask<>(thread); // 适配类
          new Thread(futureTask, "A").start();
          new Thread(futureTask, "B").start(); // 结果会被缓存,效率高
          Integer o = futureTask.get(); // 这个get 方法可能会产生阻塞!把他放到最后
          // 或者使用异步通信来处理!
          System.out.println(o);
      }
  }
  

创建 Callable 实例:MyThread thread = new MyThread();
        创建 FutureTask 实例:FutureTask futureTask = new FutureTask<>(thread);
        启动线程:
                new Thread(futureTask, "A").start();
                new Thread(futureTask, "B").start();
        获取结果:Integer o = futureTask.get();
        打印结果:System.out.println(o); 

 3.call() 方法的执行情况

线程 A 和线程 B 启动
        两个线程 A 和 B 都启动并尝试执行 futureTask。
        futureTask 包装了 MyThread 实例,因此两个线程都会调用 MyThread 的 call() 方法。
call() 方法的执行
        第一次调用:假设线程 A 先获取到 futureTask 的锁并执行 call() 方法。
                打印 "call()"
                返回 1024
                结果被缓存
第二次调用:线程 B 尝试执行 call() 方法,但由于结果已被缓存,call() 方法不会再次执行。
        直接返回缓存的结果 1024

输出结果:

Java多线程:Callable解析(附实战案例)_第2张图片

call() 方法的打印次数:
只会打印一次 "call()",因为 FutureTask 会缓存 Callable 任务的结果,后续的调用不会重新执行 call() 方法。 

总结
  • Callable 接口:允许任务返回结果并抛出异常。
  • FutureTask 类:包装 Callable 任务,提供异步执行和结果获取的功能。
  • 缓存机制:FutureTask 会缓存 Callable 任务的结果,避免重复执行。
  • call() 方法:只会执行一次,结果被缓存后,后续调用不会重新执行。
  • 特性 Runnable Callable
    返回值 ❌ 无 ✅ 有
    异常处理 ❌ 只能try-catch ✅ 可抛出
    方法签名 void run() V call() throws
    适用场景 简单异步任务 需要结果的复杂任务

三、必知必会的两大特性

1. 结果缓存机制

FutureTask task = new FutureTask<>(() -> "结果只能获取一次");
new Thread(task).start();

System.out.println(task.get()); // 输出:结果只能获取一次
System.out.println(task.get()); // 仍然输出相同结果
// 注意:任务只会执行一次,多次get()获取的是缓存结果

 2. 阻塞式获取结果

FutureTask task = new FutureTask<>(() -> {
    TimeUnit.SECONDS.sleep(5);
    return 42;
});
new Thread(task).start();

System.out.println("开始等待结果...");
Integer result = task.get(); // 这里会阻塞5秒
System.out.println("得到结果:" + result);

高频面试题精解

Q:为什么Callable不能直接传给Thread?
A:Thread构造函数只接受Runnable类型。Callable需要FutureTask这个"适配器"来桥接,就像Type-C转接头能让USB设备连接手机一样。

Q:如何处理Callable的异常?
A:两种方式:

// 方式1:在call()方法内部处理
Callable c = () -> {
    try {
        // 可能出错的代码
    } catch (Exception e) {
        return "错误处理结果";
    }
};

// 方式2:通过Future.get()捕获
try {
    future.get();
} catch (ExecutionException e) {
    Throwable cause = e.getCause(); // 获取原始异常
}

总结与升华

通过本文的学习,我们掌握了:

  1. 线程创建四大法

    • 单线程简单任务用Thread/Runnable

    • 需要返回值用Callable+FutureTask黄金组合

    • 高并发场景必用线程池

  2. Callable三大优势

    • 能带回任务结果(就像外卖小哥送餐后带回确认签名)

    • 支持异常传播(发现问题及时上报)

    • 配合FutureTask实现异步回调

  3. FutureTask使用秘籍

    • 通过get()获取结果时记得处理阻塞

    • 利用缓存特性避免重复计算

    • 结合线程池使用效果更佳

终极实战建议

  • 耗时计算任务(如Excel导出)使用Callable

  • 需要聚合多个服务结果时用FutureTask集合

  • 重要业务务必处理ExecutionException

扩展思考:当我们需要同时处理100个异步任务时,如何优化?这时就该请出CompletableFuture和Fork/Join框架这些高级装备了,它们能让多线程编程如虎添翼!

你可能感兴趣的:(JUC,java,开发语言)