揭秘java线程池-高效利器背后的精髓(下)-进阶多线程-Callable、Future和手写线程池实战


在Java并发编程中,仅会使用线程池是远远不够的。为了充分发挥多线程的威力,我们还需要深入理解Callable、Future和FutureTask的内在机制,并且能熟练地手写一个功能完备的线程池。让我们一同攻克这座高峰,领略并发编程的无穷魅力!


一、Callable和Future: 异步编程的钥匙

在传统的多线程编程中,我们仅能通过Runnable提交没有返回值的简单任务。而Callable则赋予了我们创建"有返回值的任务"的能力,为异步编程的大门打开了一扇窗。

Future则是Callable的完美绑定,通过它我们不仅能拿到任务的执行结果,还能判断任务的执行状态,甚至是取消任务。随着异步编程日益普及,掌握这对搭档就变得尤为重要。


Callable 接口和 Future 接口经常一起使用来执行异步任务。

Callable 接口与 Runnable 接口类似,但它可以返回结果并且可以抛出异常。

Future 接口代表异步计算的结果,并且可以用来检查计算是否完成,取消计算,以及获取计算的结果。


以下是一个使用 CallableFuture 完成异步排序任务的示例代码,其中包含异步取消、异常处理和超时控制:

import java.util.Arrays;
import java.util.concurrent.*;

public class AsyncSortExample {
    public static void main(String[] args) {
        // 创建一个线程池
        ExecutorService executor = Executors.newSingleThreadExecutor();

        try {
            // 创建一个 Callable 任务,执行排序操作
            Callable<Integer[]> callable = () -> {
                try {
                    // 模拟耗时的排序任务
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                // 假设我们对数组进行排序
                Integer[] data = {5, 3, 8, 1, 2};
                Arrays.sort(data);
                return data;
            };

            // 提交任务并获取 Future 对象
            Future<Integer[]> future = executor.submit(callable);

            // 模拟其他任务或操作...
            // ...

            // 超时控制,等待结果,设置超时时间为 3 秒
            try {
                Integer[] sortedData = future.get(3, TimeUnit.SECONDS);
                System.out.println("Sorted data: " + Arrays.toString(sortedData));
            } catch (TimeoutException e) {
                System.out.println("Task timed out, attempting to cancel...");
                // 超时后尝试取消任务
                future.cancel(true);
            } catch (CancellationException e) {
                System.out.println("Task was cancelled.");
            } catch (ExecutionException e) {
                // 处理 Callable 任务中的异常
                System.out.println("Task threw an exception: " + e.getCause().getMessage());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭线程池
            executor.shutdown();
        }
    }
}

代码解释:

  1. 线程池创建:使用 Executors.newSingleThreadExecutor() 创建了一个单线程的线程池。
  2. Callable 任务定义:定义了一个 Callable 任务,该任务模拟了一个耗时的排序操作。
  3. 任务提交:通过 executor.submit(callable) 提交任务,并获得一个 Future 对象。
  4. 超时控制:使用 future.get(3, TimeUnit.SECONDS) 尝试获取结果,设置了 3 秒的超时时间。
  5. 异常处理
    • TimeoutException:如果任务执行超过 3 秒,将捕获此异常,并尝试取消任务。
    • CancellationException:如果任务已经被取消,将捕获此异常。
    • ExecutionException:如果 Callable 任务中抛出了异常,将捕获 ExecutionException 并通过 e.getCause() 获取原始异常。
  6. 取消任务:如果需要取消任务,可以使用 future.cancel(true),其中 true 参数表示中断正在执行的任务。
  7. 关闭线程池:在 finally 块中关闭线程池,释放资源。

二、FutureTask源码剖析

FutureTask的出现,为Callable和Future之间架起了一座桥梁。作为两者的融合体,它不仅可以扮演Callable执行的载体角色,自身也是一个Future实例。


在这里插入图片描述


  • FutureTask并不是直接实现Future接口的,而是通过RunnableFuture接口来间接实现- - RunnableFuture接口很简单,就是继承了Runnable和Future接口。
  • 最上层的FunctionalInterface是一个注解,拥有该注解的接口支持函数式编程,图中是Runnable接口拥有该注解。

1、Future接口
public interface Future<V> {
    // 任务取消方法
    boolean cancel(boolean mayInterruptIfRunning);
    // 判断任务是否取消
    boolean isCancelled();
    // 判断任务是否完成
    boolean isDone();
    // 阻塞获取任务结果
    V get() throws InterruptedException, ExecutionException;
    // 阻塞获取任务结果,并设置阻塞超时时间
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}


2、RunnableFuture接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}


3、FutureTask 内部原理

(1)、FutureTask 属性

public class FutureTask<V> implements RunnableFuture<V> {
    // 状态机:存在以下7中状态
    private volatile int state;
    // 新建
    private static final int NEW          = 0;
    // 任务完成中
    private static final int COMPLETING   = 1;
    // 任务正常完成
    private static final int NORMAL       = 2;
    // 任务异常
    private static final int EXCEPTIONAL  = 3;
    // 任务取消
    private static final int CANCELLED    = 4;
    // 任务中断中
    private static final int INTERRUPTING = 5;
    // 任务已中断
    private static final int INTERRUPTED  = 6;

    // 支持结果返回的任务
    private Callable<V> callable;
    
    // 任务执行结果:包含正常和异常的结果,通过get方法获取
    private Object outcome; 
    
    // 任务执行线程
    private volatile Thread runner;
    
    // 栈结构的等待队列,该节点是栈中的最顶层节点
    private volatile WaitNode waiters;
}


(2)、FutureTask属性说明

  • state状态机是FutureTask用于标记任务执行的状态情况,在源码中作者也描述了这些状态可能的变化请:
    • 任务正常执行:NEW -> COMPLETING -> NORMAL
    • 任务执行异常:NEW -> COMPLETING -> EXCEPTIONAL
    • 任务被取消:NEW -> CANCELLE
    • 任务被中断:NEW -> INTERRUPTING -> INTERRUPTED

  • outcome是任务执行完后的返回结果

  • runner是真正执行任务的worker

  • waiters是一个等待节点,而是是最顶层节点,类似头节点。FutureTask中的等待队列主要作用用于当多线程同时请求get方法获取结果时,这些线程可能要同时阻塞,因此将这些线程的阻塞信息保存在了等待节点中,并构成一个栈的等待结构。

  • Callable``FutureTask

    封装了一个 Callable 任务,并且维护了任务的执行状态和结果。它内部使用 AtomicInteger 类型的 state 变量来表示任务的状态,状态的取值有:

    • NEW (0): 初始状态,任务还未开始执行
    • COMPLETING (1): 任务正在执行,即将完成
    • NORMAL (2): 任务正常完成
    • EXCEPTIONAL (3): 任务执行异常
    • CANCELLED (4): 任务被取消。
    • INTERRUPTED (5): 任务执行被中断

4、FutureTask 同步状态转移

FutureTask 的状态转移是同步操作的核心,状态转移图大致如下:

NEW -> COMPLETING -> NORMAL
   |                  |
   |                  v
   +------> EXCEPTIONAL <------+
                        |
                        v
                   CANCELLED
  • NEWCOMPLETING:当任务开始执行时,状态变为 COMPLETING
  • COMPLETINGNORMALEXCEPTIONAL:任务执行完毕后,根据是否有异常,状态变为 NORMALEXCEPTIONAL
  • NEWCANCELLEDINTERRUPTED:任务在执行前或执行中可以被取消或中断。

5、FutureTask任务执行流程

(1)、FutureTask 通过构造函数接收一个 Callable 对象。

(2)、任务通过 run() 方法启动,该方法被线程池中的线程调用。

(3)、如果任务正常执行,执行结果通过 set() 方法设置,状态变为 NORMAL

(4)、如果任务执行过程中抛出异常,异常通过 setException() 方法设置,状态变为 EXCEPTIONAL

(5)、如果任务在执行前或执行中被取消,状态变为 CANCELLED


6、使用示例
import java.util.concurrent.*;

public class FutureTaskExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        FutureTask<Integer> futureTask = new FutureTask<>(() -> {
            try {
                // 模拟耗时操作
                Thread.sleep(2000);
                return 42; // 任务返回结果
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return null;
            }
        });

        // 将 FutureTask 作为 Runnable 任务提交给线程池
        executor.execute(futureTask);

        try {
            // 获取任务结果,会阻塞直到任务完成
            Integer result = futureTask.get();
            System.out.println("Task result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            // 关闭线程池
            executor.shutdown();
        }
    }
}

以下是 FutureTask 类中一些关键方法的简要说明:

  • protected void set(Object x): 设置任务执行结果,并通知所有等待的线程。
  • protected void setException(Throwable t): 设置任务执行异常,并通知所有等待的线程。
  • public boolean run(): 如果任务尚未启动,则执行任务。
  • public boolean cancel(boolean mayInterruptIfRunning): 取消任务,如果任务正在运行,可以选择中断执行线程。
  • public Object get(): 等待任务完成并获取结果。
  • public Object get(long timeout, TimeUnit unit): 等待任务完成直到超时时间,并获取结果。

FutureTask 的实现确保了任务的执行是线程安全的,并且结果只能被设置一次。它通过内部同步机制来管理状态转换和结果访问。


三、手写一个线程池

有了对Callable、Future和FutureTask的把握,我们就可以实战一下手写线程池了。一个高效的线程池,除了基本的任务执行功能外,还需要具备以下特性:

  • 线程池大小合理控制
  • 线程复用和缓存队列
  • 拒绝策略和异常处理
  • 生命周期管理

让我们用代码一一拆解并实现上述功能:

创建一个简单的线程池需要实现多个组件和逻辑。下面是一个简化版的线程池实现。


1. 线程池框架设计

首先,定义线程池的基础结构和接口。

public class SimpleThreadPool {
    private final int corePoolSize;
    private final BlockingQueue<Runnable> workQueue;
    private final RejectedExecutionHandler handler;
    private volatile boolean isShutDown = false;
    private final List<Worker> workers = new ArrayList<>();

    public SimpleThreadPool(int corePoolSize, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        this.corePoolSize = corePoolSize;
        this.workQueue = workQueue;
        this.handler = handler;
    }

    // 提交任务
    public void submit(Runnable task) {
        if (isShutDown) {
            throw new RejectedExecutionException("ThreadPool is shutdown");
        }
        Runnable r = task;
        if (r == null) throw new NullPointerException();
        if (workers.size() < corePoolSize || !workQueue.offer(r)) {
            synchronized (this) {
                if (isShutDown) throw new RejectedExecutionException("ThreadPool is shutdown");
                // 创建并启动新线程
                Worker worker = new Worker(r);
                workers.add(worker);
                worker.start();
            }
        } else {
            // 任务缓存到队列
            handler.rejectedExecution(r, this);
        }
    }

    // 关闭线程池
    public void shutdown() {
        isShutDown = true;
    }

    // 钩子方法
    protected void beforeExecute(Thread t, Runnable r) {
    }

    protected void afterExecute(Runnable r, Throwable t) {
    }

    // 内部工作线程
    private class Worker extends Thread {
        private final AtomicBoolean running = new AtomicBoolean(false);
        private Runnable firstTask;

        public Worker(Runnable firstTask) {
            this.firstTask = firstTask;
        }

        public void run() {
            try {
                while (running.get()) {
                    try {
                        Runnable task = firstTask != null ? firstTask : workQueue.take();
                        firstTask = null;
                        beforeExecute(Thread.currentThread(), task);

                        task.run();

                        afterExecute(task, null);
                    } catch (InterruptedException e) {
                        // 线程被中断
                    } catch (Throwable e) {
                        afterExecute(null, e);
                    }
                }
            } finally {
                workers.remove(this);
            }
        }
    }
}

2. 工作线程设计

工作线程是通过继承 Thread 类并重写 run 方法来设计的,如上 Worker 类所示。


3. 任务缓存队列实现

任务缓存队列可以使用 BlockingQueue 接口实现,这里使用 LinkedBlockingQueue

BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();

4. 提交任务及执行流程

任务提交通过 submit 方法,它将任务添加到工作队列,如果工作线程少于核心大小,则创建新线程。


5. 线程池生命周期管理

线程池的生命周期通过 isShutDown 变量管理,shutdown 方法将此变量设置为 true


6. 拒绝策略实现

拒绝策略通过 RejectedExecutionHandler 接口实现,自定义拒绝行为。

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, SimpleThreadPool executor);
}

// 示例拒绝策略
public class CallerRunsPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, SimpleThreadPool executor) {
        r.run();
    }
}

7. 钩子方法设计

beforeExecuteafterExecute 是线程池执行每个任务前后的钩子方法。


8. 测试用例
public class SimpleThreadPoolTest {
    public static void main(String[] args) {
        SimpleThreadPool pool = new SimpleThreadPool(3, new LinkedBlockingQueue<>(), new CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            int finalI = i;
            pool.submit(() -> System.out.println("Task " + finalI + " executed by " + Thread.currentThread().getName()));
        }

        pool.shutdown();
    }
}

关键逻辑和代码实现技巧

  • 线程安全:使用 AtomicBooleansynchronized 块来确保线程安全。
  • 任务队列:使用 BlockingQueue 来缓存任务,确保任务不会丢失。
  • 拒绝策略:当任务无法处理时,自定义拒绝策略,例如可以运行任务、丢弃任务或抛出异常。
  • 生命周期管理:通过一个变量控制线程池的状态,确保在关闭时不接受新任务。
  • 钩子方法:允许在任务执行前后执行额外的操作,如日志记录或资源管理。

请注意,这是一个非常基础的线程池实现,用于教学目的。在生产环境中,建议使用 Java 内置的 java.util.concurrent 包中的 ThreadPoolExecutor,它提供了更全面的功能和优化。


四、生产实践中的最佳实践

通过前面的理论学习和动手实战,相信大家已经比较扎实地掌握了Callable、Future以及如何手写一个线程池。不过,在生产实践中还需要注意哪些地方呢?让我结合过往的经验,分享一些最佳实践建议:


1. 线程池参数设置

核心池大小(Core Pool Size):

  • 核心池大小应根据应用程序的性质和预期的工作负载来设置。例如,对于主要执行CPU密集型任务的应用程序,核心池大小应设置为等于机器的核心数,以避免过多的线程上下文切换。

  • 例子:

    new ThreadPoolExecutor(4, 8, ...)
    
    • 对于一个有4个CPU核心的服务器,可以将核心池大小设置为4。

最大池大小(Maximum Pool Size):

  • 最大池大小应考虑到系统资源和任务的特性。如果任务可以等待,可以适当增加最大池大小,以便在高峰时段处理更多任务。

  • 例子:

    new ThreadPoolExecutor(..., 16, ...)
    
    • 对于I/O密集型任务,最大池大小可以设置得更高,比如16。

工作队列(Work Queue):

  • 选择合适的工作队列类型和大小。对于有界队列,可以避免内存溢出问题,但也可能导致任务拒绝。

  • 例子:

    new LinkedBlockingQueue<>(100)
    
    • 使用有界队列,大小为100,可以缓存100个待处理任务。

线程存活时间(Keep-Alive Time):

  • 设置非核心线程的存活时间,以回收长时间空闲的线程。

  • 例子:

    new ThreadPoolExecutor(..., 1, TimeUnit.MINUTES, ...)
    
    • 非核心线程空闲1分钟后将被终止。

线程工厂(Thread Factory):

  • 使用自定义线程工厂设置有意义的线程名称,有助于日志记录和问题排查。

  • 例子:

    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("my-pool-%d").build();
    

2. 任务提交优化

批量任务提交:

  • 如果有大量的任务需要提交,考虑使用批量提交而不是单独提交每个任务,以减少同步开销。
  • 例子:使用 ExecutorCompletionService 来批量提交和处理任务。

优先级队列:

  • 如果任务有优先级,可以使用优先级队列(PriorityBlockingQueue)来确保高优先级的任务先被执行。

任务依赖性:

  • 对于有依赖关系的任务,可以使用 Future 对象来安排依赖任务的提交。

避免提交空任务:

  • 避免提交 null 任务,这将导致线程池抛出 NullPointerException
3. 异常处理

捕获和记录异常:

  • 在任务执行时捕获并记录异常,避免任务因未捕获的异常而失败。

  • 例子:

    Runnable task = () -> {
        try {
            // 任务代码
        } catch (Exception e) {
            // 记录异常
        }
    };
    

自定义拒绝策略:

  • 实现自定义拒绝策略来处理无法接受的任务,例如,记录日志、重试或丢弃任务。

  • 例子:

    ThreadPoolExecutor executor = new ThreadPoolExecutor(..., new ThreadPoolExecutor.CallerRunsPolicy());
    

线程中断:

  • 正确处理线程中断,避免在任务执行中出现 InterruptedException

  • 例子:

    public void run() {
        try {
            // 任务代码
        } catch (InterruptedException e) {
            // 处理中断
        } finally {
            // 清理资源
        }
    }
    

4、实战经验总结
  • 监控和度量: 监控线程池的状态和性能指标,如活跃线程数、任务队列大小等。
  • 资源隔离: 对于不同类型的任务,考虑使用不同的线程池,以避免资源争抢。
  • 优雅关闭: 在应用程序关闭时,提供足够的时间让线程池中的线程优雅地关闭。
  • 持续优化: 根据应用程序的实际运行情况,持续调整线程池参数。

通过遵循这些最佳实践,可以有效地提高线程池的性能,减少资源浪费,并确保应用程序的稳定性和可维护性。


五、结语

走过如此漫长的篇幅,我们已将关于Callable、Future和线程池实战的知识版图全景勾勒出来。然而,多线程编程之路终究还很长,比如如何利用CompletableFuture优雅地组合并发任务?又该如何解决死锁等并发难题?更多精彩等待我们一同去发现…

  • 28
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w风雨无阻w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值