沉梦听雨的编程指南 沉梦听雨的编程指南
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 基础篇
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • RPC
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 多媒体框架
  • 虚拟机
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 随笔
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Axios入门
  • Vue Router入门
  • Pinia入门
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
Github (opens new window)

沉梦听雨

时间是最好的浸渍剂,而沉淀是最好的提纯器🚀
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 基础篇
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • RPC
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 多媒体框架
  • 虚拟机
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 随笔
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Axios入门
  • Vue Router入门
  • Pinia入门
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
Github (opens new window)
  • 基础篇

  • 集合篇

  • 并发篇

    • Java并发基础小结
    • 锁详解
    • Synchronized和Volatile的使用与区别
    • 线程池详解
      • 首先回顾一下单个线程的创建方式
        • 1、继承 Thread 类
        • 2、实现 Runnable 接口
        • 3、使用匿名内部类
        • 4、使用 Java 8 的 Lambda 表达式
        • 5、实现 Callable 接口
        • 实现 Runnable 接口和 Callable 接口的区别?
      • 简单介绍
        • 什么是线程池?
        • 为什么要用线程池?
        • 讲讲线程池的工作流程
        • 线程池使用入门
      • Executor 框架介绍
        • 概述
        • 结构
        • 使用流程
      • 线程池原理解析
        • 线程池有哪些参数?
        • 讲讲核心线程数和最大线程数的区别?
        • 阻塞队列有哪些?
        • 讲讲有哪些拒绝策略?
        • 新线程添加的流程?
        • 线程池的两种创建方式
        • ThreadPoolExecutor
        • Executors
        • 代码示例
        • 线程池提交 execute 和 submit 有什么区别?
        • 线程池的关闭方式
      • 学习参考
    • CompletableFuture学习
    • LocalThread学习
    • Java内存管理总结
  • JVM

  • 新特性

  • Java核心技术卷I

  • Java
  • 并发篇
沉梦听雨
2023-06-14
目录

线程池详解

# 线程池详解

# 首先回顾一下单个线程的创建方式

# 1、继承 Thread 类

这是一种比较传统的创建线程的方式。你可以创建一个类,继承自 Thread 类,并重写 run 方法来定义线程的执行逻辑。

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程的执行逻辑
    }
}

// 创建并启动线程
MyThread thread = new MyThread();
thread.start();
1
2
3
4
5
6
7
8
9
10

# 2、实现 Runnable 接口

这种方式更常用,它避免了 Java 的单继承限制,你可以实现 Runnable 接口,然后将其实例作为参数传递给 Thread 构造函数。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程的执行逻辑
    }
}

// 创建并启动线程
Thread thread = new Thread(new MyRunnable());
thread.start();
1
2
3
4
5
6
7
8
9
10

# 3、使用匿名内部类

你可以在创建线程时使用匿名内部类,实现 Runnable 接口的 run 方法。

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        // 线程的执行逻辑
    }
});
thread.start();
1
2
3
4
5
6
7

# 4、使用 Java 8 的 Lambda 表达式

如果 Runnable 接口只有一个抽象方法,你可以使用 Lambda 表达式简化代码。

Thread thread = new Thread(() -> {
    // 线程的执行逻辑
});
thread.start();
1
2
3
4

# 5、实现 Callable 接口

Callable 接口允许线程返回结果或抛出异常。需要通过 ExecutorService 来执行。

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 线程的执行逻辑
        return "Hello from Callable";
    }
}

ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future = executor.submit(new MyCallable());
String result = future.get(); // 获取线程执行结果
1
2
3
4
5
6
7
8
9
10
11

# 实现 Runnable 接口和 Callable 接口的区别?

Java 中的 Runnable 接口和 Callable 接口都是用来创建多线程的接口,它们的区别如下:

  1. 方法名不同。

    • Runnable 接口只有一个 run() 方法,
    • 而 Callable 接口只有一个 call() 方法。
  2. 返回值不同。

    • Runnable 的 run() 方法没有返回值,

    • 而 Callable 的 call() 方法可以返回执行结果。

  3. 异常处理不同。

    • Runnable 的 run() 方法不能抛出异常,
    • 而 Callable 的 call() 方法可以抛出异常,并且需要在调用 Future.get() 方法时进行异常处理。
  4. 调用方式不同。

    • Runnable 接口可以通过 Thread 类的构造方法来创建一个新的线程并启动它,
    • 而 Callable 接口则需要借助 Executor 框架来执行。
  5. 用途不同。

    • Runnable 接口通常用于需要执行一些简单的任务的场景,
    • Callable 接口通常用于需要返回结果、或者需要抛出异常、或者需要在执行任务前进行一些初始化操作的场景。

# 简单介绍

# 什么是线程池?

线程池就是管理一系列线程的资源池。

当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。

# 为什么要用线程池?

简单来说,是因为使用线程池可以提高资源的利用率。

线程池可以帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。

我们写代码的过程中,学会池化思想,最直接相关的就是使用线程池而不是去new一个线程。

使用线程池有三大好处:

  1. 提高响应速度。通过线程池创建一系列线程,使用时直接通过线程池获取,不再需要手动创建线程,响应速度自然就大大提高了。
  2. 降低资源消耗。由于线程池被池化管理了,我们无需为了某些功能去手动创建和销毁线程,资源消耗自然降低。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

# 讲讲线程池的工作流程

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会根据拒绝策略来对应处理。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

图解:

image

# 线程池使用入门

  1. 首先创建一个含有 3 个线程的线程,
  2. 然后提交 3 个任务到线程池中,让线程池中的线程池执行,
  3. 完成后通过 shutdown 停止线程池,线程池收到通知后会将手头的任务都执行完,再将线程池停止。

这里使用 isTerminated 判断线程池是否完全停止了。只有状态为 terminated 才能说明线程池关闭了,结束循环,退出方法。

    @Test
    void contextLoads() {
        // 创建含有3个线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        // 提交3个任务到线程池中
        for (int i = 0; i < 3; i++) {
            final int taskNo = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("执行任务{}", taskNo);
            });
        }

        // 关闭线程池
        threadPool.shutdown();
        System.out.println("线程池已关闭");
        // 如果线程池还没达到Terminated状态,说明线程池中还有任务没有执行完,则继续循环等待线程池执行完任务
        while (!threadPool.isTerminated()) {

        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

输出结果

线程池已关闭
15:59:02.548 [pool-1-thread-1] INFO com.chenmeng.project.threadPool.ThreadPoolTest - 执行任务0
15:59:02.548 [pool-1-thread-3] INFO com.chenmeng.project.threadPool.ThreadPoolTest - 执行任务2
15:59:02.548 [pool-1-thread-2] INFO com.chenmeng.project.threadPool.ThreadPoolTest - 执行任务1
1
2
3
4

# Executor 框架介绍

# 概述

在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。

这是因为线程池的执行过程会等待构造完成后再进行任务的执行,从而避免了 this 逃逸问题的发生。

什么是 this 逃逸问题?

在 Java 中,对象的构造过程可能会涉及到多个线程,而当一个对象尚未完全构造完成但已经被其他线程引用时,就可能产生 this 逃逸问题。

具体来说,当一个对象正在构造过程中,它的引用就被发布到了其他线程,这时其他线程可能会使用这个尚未完全构造的对象,从而导致意料之外的行为和错误。这可能会因为对象的状态不稳定而引发线程安全问题。

# 结构

Executor 框架结构主要由三大部分组成:

  1. 任务。包括被执行任务需要实现的接口:Runnable 接口或 Callable 接口。
  2. 任务的执行。包括任务执行机制的核心接口 Executor,以及继承自 Executor 的 ExecutorService 接口。
    • Executor 框架有两个关键类实现了 ExecutorService 接口
    • ThreadPoolExecutor
    • ScheduleThreadPoolExecutor
  3. 异步计算的结果。包括接口 Future 和实现 Future 接口的 FutureTask 类。

# 使用流程

  1. 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。

  2. 把创建完成的实现 Runnable/Callable 接口的【对象】直接交给 ExecutorService 执行:

    • ExecutorService.execute(Runnable command)

    • 或者执行(ExecutorService.submit(Runnable task)

    • 或者执行 ExecutorService.submit(Callable <T> task))

  3. 如果执行 ExecutorService.submit(…),ExecutorService 将返回一个实现 Future 接口的对象(我们刚刚也提到过了执行 execute() 方法和 submit() 方法的区别,submit() 会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。

  4. 最后,主线程可以执行 FutureTask.get() 方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

代码示例:

    @Test
    void testExecutor2() {
        // 创建一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 创建实现Runnable接口的任务
        Runnable task1 = () -> {
            System.out.println("Task 1 is running on thread: " + Thread.currentThread().getName());
        };

        // 创建实现Callable接口的任务
        Callable<String> task2 = () -> {
            System.out.println("Task 2 is running on thread: " + Thread.currentThread().getName());
            return "Task 2 Result";
        };

        try {
            // 执行Runnable任务
            executorService.execute(task1);

            // 提交Callable任务,并获取Future对象
            Future<String> future = executorService.submit(task2);

            // 主线程等待Callable任务执行完成,并获取结果
            String result = future.get();
            System.out.println("Task 2 Result: " + result);

        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            // 关闭线程池
            executorService.shutdown();
        }
        
        // 输出
        /*
        Task 1 is running on thread: pool-1-thread-1
        Task 2 is running on thread: pool-1-thread-2
        Task 2 Result: Task 2 Result
        */
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 线程池原理解析

# 线程池有哪些参数?

通过 Executors 框架创建的线程池,从源码可以看到,它底层是通过 ThreadPoolExecutor 完成线程池的创建,具体参数如下:

    // 创建一个线程池,该线程池重用在共享无界队列上操作的固定数量的线程。在任何时候,最多有 nThreads 线程处于活动状态,正在处理任务。如果在所有线程都处于活动状态时提交额外的任务,它们将在队列中等待,直到一个线程可用。如果任何线程在关闭之前的执行过程中由于失败而终止,如果需要执行后续任务,则会有一个新线程取代它的位置。在显式关闭池之前,池中的线程将一直存在。
	// nThreads -线程池中的线程数
	public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

    public class ThreadPoolExecutor extends AbstractExecutorService {
        // ...
        
        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                    Executors.defaultThreadFactory(), defaultHandler);
        }
        
        // ...
    }

	// 默认线程工厂
    private static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

    // 默认拒绝策略
    private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

    public static class AbortPolicy implements RejectedExecutionHandler {
        
        public AbortPolicy() { }
        
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
  1. corePoolSize:线程池的核心线程数,即线程池中始终保持的线程数。

  2. maximumPoolSize:线程池中最大的线程数,包括核心线程数和非核心线程数。

  3. keepAliveTime:非核心线程的闲置时间,超过该时间后将被回收。

  4. unit:keepAliveTime 非核心线程的闲置时间的单位。

  5. workQueue:任务队列,用于存储还未被执行的任务。

  6. threadFactory:线程工厂,用于创建线程。

  7. handler:饱和策略,即当线程池中的线程都在执行任务时,新的任务会如何处理。(也称为拒绝策略)

# 讲讲核心线程数和最大线程数的区别?

核心线程数和最大线程数的区别在于:

  • 当任务提交时,线程池会优先创建核心线程来执行任务,
  • 只有当任务队列已满且核心线程都在执行任务时,才会创建非核心线程来执行任务,直到达到最大线程数为止。

# 阻塞队列有哪些?

Java 中常用的阻塞队列有以下 4 种:

  1. ArrayBlockingQueue:
    • 特点:一个基于数组的有界阻塞队列,按先进先出 (FIFO) 原则对元素进行排序。
    • 适用场景:适用于需要严格控制内存使用的场景,因为它总是有界的。
    • 优点:提供公平性保证(可选),并且由于其固定大小,可以防止无限增长。
    • 缺点:一旦队列满,新任务将被拒绝,除非使用适当的拒绝策略。
  2. LinkedBlockingQueue:
    • 特点:一个基于链表的有界或无界阻塞队列,按先进先出 (FIFO) 原则对元素进行排序。
    • 适用场景:当任务到达速率相对平稳且可以预测时使用。如果指定了容量,则它是一个有界的队列;如果不指定容量,默认是无界的(实际上是有一个非常大的默认值)。
    • 优点:提供了较好的吞吐量,并且可以在内存允许的情况下容纳大量任务。
    • 缺点:无界队列可能导致内存溢出,尤其是在高负载情况下。
  3. PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
    • 特点:一个支持带有优先级排序的无界阻塞队列。
    • 适用场景:当任务具有不同优先级并且你希望优先处理高优先级任务时使用。
    • 优点:可以根据自定义比较器来决定任务的优先级。
    • 缺点:由于是无界的,可能会导致内存问题;此外,维护有序性也增加了额外开销。
  4. SynchronousQueue:
    • 特点:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的对应移除操作,否则插入操作一直处于阻塞状态。
    • 适用场景:适用于直接移交模式,即生产者线程直接将任务交给消费者线程,没有中间缓冲区。
    • 优点:最小化了延迟,因为任务几乎立即被处理。
    • 缺点:增加了线程之间的耦合度,并且如果所有工作线程都在忙碌状态,新的任务会立即被拒绝。

# 讲讲有哪些拒绝策略?

有四种常见的拒绝策略:

  1. AbortPolicy 终止策略(默认):
    • 行为:直接抛出异常 RejectedExecutionException,阻止系统正常运行。
    • 适用场景:当不想丢失任何任务时使用,但需要注意这会导致调用方代码中必须捕获并处理异常。
  2. CallerRunsPolicy 调用者运行策略:
    • 行为:由调用线程(提交任务的线程)执行该任务,这将降低新任务的提交速度。
    • 适用场景:适合短期过载情况,能够减轻系统压力,同时不会丢弃任务。
  3. DiscardOldestPolicy 丢弃最旧策略:
    • 行为:抛弃队列中最旧的任务,然后尝试重新提交当前任务。
    • 适用场景:当希望尽可能多地保留最近的任务时使用,但这可能导致一些较早提交的任务被忽略。
  4. DiscardPolicy 直接丢弃策略:直接丢弃任务,不做任何处理。
    • 行为:默默地丢弃任务而不做任何处理。
    • 适用场景:当你愿意接受某些任务被丢弃时使用,例如日志记录等非关键任务。

最常用的组合

在实际项目中,最常用的组合通常是:

  • LinkedBlockingQueue + CallerRunsPolicy 或 AbortPolicy:
    • 这种组合既提供了良好的吞吐量又能在资源紧张时通过合理的拒绝策略保护系统免受过载的影响。
  • ArrayBlockingQueue + CallerRunsPolicy:
    • 对于那些需要严格控制内存使用并且可以容忍一定程度上任务延迟的应用程序来说,这是一个不错的选择。
  • SynchronousQueue + CallerRunsPolicy:
    • 在高响应性的应用程序中,这种组合可以减少延迟,但要求有足够的空闲线程来接收任务。

# 新线程添加的流程?

新线程的添加有以下 4 个流程:

  1. 如果当前线程池中的线程数小于核心线程数,那么就创建一个新的核心线程来执行这个任务;

  2. 如果当前线程池中的线程数已经达到了核心线程数,那么就将任务添加到任务队列中等待执行;

  3. 如果任务队列已满,但当前线程池中的线程数还没有达到最大线程数,那么就创建一个新的非核心线程来执行这个任务;

    非核心线程在执行完任务并超过闲置时间之后会被回收,直到线程池中的线程数又重新降至核心线程数。

  4. 如果当前线程池中的线程数已经达到了最大线程数,那么就根据饱和策略来处理这个任务。

# 线程池的两种创建方式

# ThreadPoolExecutor

方式一:通过 ThreadPoolExecutor 构造函数来创建(推荐)。

我们可以创建多种类型的 ThreadPoolExecutor:

  • FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor:该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
  • ScheduledThreadPool:该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。

# Executors

方式二:通过 Executor 框架的工具类 Executors 来创建。

Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor:使用的是无界的 LinkedBlockingQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

  • CachedThreadPool:使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。

  • ScheduledThreadPool 和 SingleThreadScheduledExecutor : 使用的无界的延迟阻塞队列 DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE, 可能堆积大量的请求,从而导致 OOM。

    OOM(Out of Memory)是指内存溢出,即程序在运行过程中申请的内存超过了 JVM 所能提供的最大内存限制,导致无法继续分配内存,从而抛出内存溢出异常。

# 代码示例

    /**
     * 通过 ThreadPoolExecutor 构造函数创建(推荐)
     */
    @Test
    void testCreateThreadPool() {
        // 创建一个固定大小为3的线程池
        ThreadPoolExecutor fixedPool = new ThreadPoolExecutor(
                3, // 核心线程数
                3, // 最大线程数
                0L, TimeUnit.MILLISECONDS, // 空闲线程存活时间(设置为 0L 意味着非核心线程在完成当前任务后将立即终止,不会等待任何额外的时间。)
                new LinkedBlockingQueue<>(10), // 阻塞队列,容量为10
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        // 创建一个单一线程的线程池
        ThreadPoolExecutor singlePool = new ThreadPoolExecutor(
                1, // 核心线程数
                1, // 最大线程数
                0L, TimeUnit.MILLISECONDS, // 空闲线程存活时间
                new LinkedBlockingQueue<>(), // 无界阻塞队列
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );

        // 创建一个可缓存的线程池
        ThreadPoolExecutor cachedPool = new ThreadPoolExecutor(
                0, // 核心线程数
                Integer.MAX_VALUE, // 最大线程数
                60L, TimeUnit.SECONDS, // 空闲线程存活时间
                new SynchronousQueue<>(), // 同步队列
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        // 创建一个支持定时任务的线程池,指定核心线程数为2
        ScheduledThreadPoolExecutor scheduledPool = new ScheduledThreadPoolExecutor(2);
    }

    /**
     * 通过 Executor 框架的工具类 Executors 来创建(不推荐,可能导致 OOM)
     */
    @Test
    void testCreateThreadPool2() {
        // 固定大小的线程池
        ExecutorService fixedPool = Executors.newFixedThreadPool(3);
        // 单一线程的线程池
        ExecutorService singlePool = Executors.newSingleThreadExecutor();
        // 可缓存的线程池
        ExecutorService cachedPool = Executors.newCachedThreadPool();
        // 定时任务线程池
        ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
    }

    @Test
    void testCreateThreadPool3() {
        // 创建一个自定义的 ThreadPoolExecutor
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60L, TimeUnit.SECONDS, // 空闲线程存活时间
                new LinkedBlockingQueue<>(10), // 阻塞队列,容量为10
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        // 提交任务给线程池
        for (int i = 0; i < 15; i++) {
            int taskId = i;
            executor.submit(() -> {
                System.out.println("Executing Task " + taskId);
                try {
                    Thread.sleep(2000); // 模拟任务执行时间
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();

        try {
            // 等待所有任务完成,最多等待60秒
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                System.out.println("Tasks did not terminate in the specified time.");
            } else {
                System.out.println("All tasks have been completed.");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

# 线程池提交 execute 和 submit 有什么区别?

execute() 和 submit() 是 Java 并发包中 Executor 接口及其子接口(如 ExecutorService)提供的两种方法,用于提交任务给线程池执行。虽然它们都允许你将任务交给线程池处理,但它们之间存在一些重要的区别:

1、返回值

  • execute(Runnable command):

    • 签名:void execute(Runnable command)
    • 功能:接受一个 Runnable 对象作为参数,并在某个线程中执行该任务。
    • 返回值:没有返回值(void)。这意味着当你调用 execute() 方法时,你无法直接获取任务的执行结果或状态。
  • submit(Runnable task) 或 submit(Callable<T> task):

    • 签名:
      • Future<?> submit(Runnable task)
      • <T> Future<T> submit(Callable<T> task)
    • 功能:同样可以接受 Runnable 或 Callable 对象作为参数,并在线程池中的某个线程上执行任务。
    • 返回值:返回一个 Future 对象,通过它可以检查任务的状态、等待任务完成以及获取任务的结果(如果是 Callable 提交的任务)。

2、异常处理

  • execute():

    • 如果任务抛出了未捕获的异常,这些异常通常会被线程池内部处理,默认情况下可能会被记录到日志中,但是不会传播回调用者。
  • submit():

    • 对于 Callable 提交的任务,如果任务抛出了异常,Future.get() 方法会将异常封装为 ExecutionException 并抛出给调用者。
    • 对于 Runnable 提交的任务,异常行为类似于 execute(),但如果使用了 Future 来跟踪任务,则可以通过 get() 方法来捕捉异常。

3、使用场景

  • execute():

    • 当你不关心任务的结果或状态,只需要确保任务被执行时,可以使用 execute() 方法。这适用于简单的后台任务,例如日志记录、发送通知等不需要返回结果的操作。
  • submit():

    • 当你需要跟踪任务的状态、等待任务完成或者获取任务的结果时,应该使用 submit() 方法。它更适合那些需要同步处理结果或对任务执行进行更细粒度控制的情况。

4、示例代码

    @Test
    void testExecutor() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 使用 execute()
        executorService.execute(() -> System.out.println("Executing a simple task"));

        // 使用 submit() with Runnable
        Future<?> future1 = executorService.submit(() -> System.out.println("Submitting a task without result"));
        future1.get(); // 等待任务完成,但不关心结果

        // 使用 submit() with Callable
        Future<Integer> future2 = executorService.submit(() -> {
            System.out.println("Submitting a task with result");
            return 42;
        });
        Integer result = future2.get(); // 获取任务结果
        System.out.println("Task result: " + result);

        executorService.shutdown();

        // 输出
        /*
        Executing a simple task
        Submitting a task without result
        Submitting a task with result
        Task result: 42
        */
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

5、总结

选择 execute() 还是 submit() 取决于你的具体需求:

  • 如果只是想让任务尽快开始执行而不关心其结果或状态,那么 execute() 是个简单而直接的选择。
  • 如果你需要对任务有更多的控制,比如等待任务完成、获取任务结果或处理任务执行期间发生的异常,那么 submit() 更加适合。

# 线程池的关闭方式

线程池的停止方式有两种:

  1. shutdown: 使用这个方法之后,我们无法提交新的任务进来,线程池会继续工作,将手头的任务执行完再停止。
  2. shutdownNow: 这种停止方式比较粗暴,线程池会直接将手头的任务都强行停止,且不接受新任务进来,线程停止立即生效。

# 学习参考

  • Java 线程池详解 | JavaGuide) (opens new window)
  • Java线程池详解 | Shark Chili (opens new window)
  • 面渣逆袭-Java并发编程 (opens new window)
  • Executor框架详解 (opens new window)
  • 实战总结!18种接口优化方案的总结 (opens new window)
上次更新: 2025/5/25 01:33:09
Synchronized和Volatile的使用与区别
CompletableFuture学习

← Synchronized和Volatile的使用与区别 CompletableFuture学习→

Theme by Vdoing | Copyright © 2023-2025 沉梦听雨 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式