首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Java 的ee 初阶——线程池

Java 的ee 初阶——线程池

作者头像
Han.miracle
发布2025-12-23 09:32:15
发布2025-12-23 09:32:15
1210
举报

线程池

1、线程池是什么 虽然创建线程/销毁线程的开销

想象这么⼀个场景:

        在学校附近新开了一家快递店,⽼板很精明,想到一个与众不同的办法来经营。店里没有雇人,而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。这个类比我们平时来一个任务,起一个线程进行处理的模式。

        很快老板发现问题来了,每次招聘+解雇同学的成本还是非常高的。老板还是很善于变通的,知道了为什么大家都要雇人了,所以指定了一个指标,公司业务人员会扩张到3个人,但还是随着业务逐步雇人。于是再有业务来了,老板就看,如果现在公司还没3个人,就雇一个人去送快递,否则只是把业务放到一个本本上,等着3个快递人员空闲的时候去处理。这个就是我们要带出的线程池的模式。

线程池最大的好处就是减少每次启动、销毁线程的损耗。

线程池和协程(纤程)的技术

核心是解释 “为何需要线程池和协程”:

1. 技术背景脉络
  • 最初引入线程的原因:为了解决 “频繁创建销毁进程太慢” 的问题,线程比进程更轻量,创建销毁开销更小。
  • 现状需求升级:随着互联网发展,性能要求进一步提高,“频繁创建销毁线程” 的开销也变得不可接受。
2. 解决方案

这里为什么说Go语言能够威胁到Java的地位,就是他更擅长处理并发编程

1.代码更加简单(CSP编程模型)

2.使用协程,相比于线程性能更高

2、标准库中的线程池         使用Executors.newFixedThreadPool(10)能创建出固定包含10个线程的线程池。

        返回值类型为ExecutorService。

        通过ExecutorService.submit可以注册一个任务到线程池中。

代码语言:javascript
复制
 ExecutorService pool = Executors.newFixedThreadPool(10);
 pool.submit(new Runnable() {
     @Override
     public void run() {
         System.out.println("hello");
     }
 });

Executors 创建线程池的几种方式:

        newFixedThreadPool:创建固定线程数的线程池

        newCachedThreadPool:创建线程数目动态增长的线程池

        newSingleThreadExecutor: 创建只包含单个线程的线程池

        newScheduledThreadPool:设定延迟时间后执行命令,或者定期执行命令,是进阶版的Timer。

Executors 本质上是ThreadPoolExecutor类的封装。

ThreadPoolExecutor 提供了更多的可选参数,可以进⼀步细化线程池行为的设定

  corePoolSize:正式员工的数量。(正式员工,一旦录用,永不辞退)

        maximumPoolSize:正式员工+临时工的数目。(临时工:一段时间不干活,就被辞退)

        keepAliveTime:临时工允许的空闲时间

        unit:keepaliveTime 的时间单位,是秒,分钟,还是其他值

        workQueue:传递任务的阻塞队列

        threadFactory:创建线程的工厂,参与具体的创建线程工作,通过不同线程工厂创建出的线程相当于对一些属性进行了不同的初始化设置

        RejectedExecutionHandler:拒绝策略,如果任务量超出公司的负荷了接下来怎么处理

                AbortPolicy(): 超过负荷,直接抛出异常

                CallerRunsPolicy():调用者负责处理多出来的任务

                DiscardOldestPolicy():丢弃队列中最⽼的任务

                DiscardPolicy():丢弃新来的任务

3、实现线程池         核心操作为submit,将任务加入线程池中

        使用Worker类描述一个工作线程,使用Runnable描述一个任务

        使用一个BlockingQueue组织所有的任务

        每个worker线程要做的事情:不停的从BlockingQueue中取任务并执行

        指定一下线程池中的最大线程数maxWorkerCount:当当前线程数超过这个最大值时,就不再新增线程了

代码语言:javascript
复制
class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    // 通过这个⽅法, 来把任务添加到线程池中. 
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
    // n 表⽰线程池⾥有⼏个线程. 
    // 创建了⼀个固定数量的线程池. 
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while (true) {
                    try {
                        // 取出任务, 并执⾏~~ 
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
 }
代码语言:javascript
复制
 // 线程池 
 public class Demo {
     public static void main(String[] args) throws InterruptedException {
         MyThreadPool pool = new MyThreadPool(4);
         for (int i = 0; i < 1000; i++) {
             pool.submit(new Runnable() {
                 @Override
                 public void run() {
                     // 要执⾏的⼯作
                     System.out.println(Thread.currentThread().getName() + " hello");
                 }
             });
         }
     }
 }

RejectedExectionHandler handler 拒绝策略

整个线程七个参数中,最重要,最复杂的

面试官考察,线程池的参数含义,最想听的就是你对第七个参数的理解

submit  把创建的线程提交到任务队列当中去,任务队列的实现是用阻塞队列

队列满了,在添加,就会阻塞~~

但是我们不希望再这里阻塞太多

对于线程池来说,发现入队列操作时,队列满了,不会真的触发“入队列操作”,不会真真阻塞,而且执行拒绝策略相关的代码

也就是这四个策略

所以,Java 标准库,也提供了另一组类,针对 ThreadPoolExecutor 进行了进一步封装,简化线程池的使用.

  • Executors.newFixedThreadPool(4);核心线程数和最大线程数一样~~
  • Executors.newCachedThreadPool();最大线程数是一个很大的数字.(线程可以无限增加)

工厂模式有什么作用的?

  1. 弥补构造方法的局限性构造方法的名称必须与类名一致,且如果需要通过相同参数列表创建不同类型的实例(比如用同样的参数创建 “圆形” 和 “方形”),构造方法无法区分(因为参数列表相同会导致编译错误)。而工厂模式可以通过不同的工厂方法名(如 createCircle()createSquare()),用相同参数创建不同实例,解决了构造方法重载的限制。
  2. 隐藏对象创建细节:用户无需知道对象创建的复杂逻辑(如初始化步骤、依赖注入等),只需调用工厂方法即可,降低耦合。
  3. 统一管理对象创建:将对象创建逻辑集中在工厂类中,便于修改(如更换实例类型、添加缓存机制等),符合 “开闭原则”。
  4. 支持多态创建:工厂可以返回接口或抽象类的不同实现类,用户面向接口编程,无需关心具体实现,提高代码灵活性。
代码语言:javascript
复制
public MyThreadPool(int n) {
    // 初始化线程池,创建固定个数的线程
    // 这里使用ArrayBlockingQueue作为任务队列,容量为1000
    queue = new ArrayBlockingQueue<>(1000);

    // 创建 N 个线程
    for (int i = 0; i < n; i++) {
        Thread t = new Thread(() -> {
            try {
                while (true) {
                    Runnable task = queue.take();
                    task.run();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

始化一个线程池的任务队列

  • ArrayBlockingQueue 是 Java 中的一个有界阻塞队列,它基于数组实现,具有固定的容量(这里设置为 1000)。
  • 在线程池场景中,它的作用是暂存待执行的任务:当提交的任务数量超过线程池的线程处理能力时,任务会被放入这个队列中,等待线程池中的线程来执行;如果队列满了,再提交任务就会触发线程池的拒绝策略。

模拟线程池?

代码语言:javascript
复制
package thread;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

// 实现一个固定线程个数的线程池
class MyThreadPool {
    private BlockingQueue<Runnable> queue = null;
    public MyThreadPool(int n) {
        // 初始化线程池,创建固定个数的线程
        // 这里使用ArrayBlockingQueue作为任务队列, 容量为1000
        queue = new ArrayBlockingQueue<>(1000);

        // 创建 N 个线程
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                try {
                    while (true) {
                        Runnable task = queue.take();
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            // t.setDaemon(true);
            t.start();
        }
    }

    public void submit(Runnable task) throws InterruptedException {
        // 将任务放入队列中
        queue.put(task);
    }
}

public class Demo35 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);

        // 向线程池提交任务
        for (int i = 0; i < 100; i++) {
            int id = i;
// 临时变量,每次循环都是新的,且不会被修改
            pool.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " id=" + id);
            });
        }
    }
}

lambda 表达式中引用的局部变量必须是最终的(final)或事实上最终的(effectively final)

所以我们选择用一个临时变量id 来保证这个是不会修改的

shutdown() 和 awaitTermination() 的区别

操作系统中关于进程退出码的约定:

  • 退出码为 0:表示进程正常结束,即程序按照预期逻辑执行完毕,没有出现错误或异常。
  • 退出码非 0:表示进程异常结束,不同的非零数字可以用来区分具体的异常原因(例如,1 可能代表某种通用错误,2 可能代表参数错误等,具体含义可由程序开发者自定义或遵循系统惯例)。

三、总结-保证线程安全的思路

1、使用没有共享资源的模型

2、适用共享资源只读,不写的模型

        a. 不需要写共享资源的模型

        b. 使用不可变对象

3、直面线程安全(重点)

        a. 保证原子性

        b. 保证顺序性

        c. 保证可见性

四,常用任务队列类型及用法

Java 提供了多种 BlockingQueue 实现,不同队列适用于不同场景,选择时需考虑是否有界(是否限制长度)、排序方式等特性:

1. LinkedBlockingQueue(默认无界,推荐)
  • 无界模式:默认不限制长度(实际受内存限制),适合任务数量波动大的场景,但需注意 OOM 风险。
  • 有界模式:通过构造参数指定容量(如 new LinkedBlockingQueue<>(100)),避免任务无限积压。
代码语言:javascript
复制
// 有界队列:最多存放 100 个任务
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(100);

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60L, TimeUnit.SECONDS,
    queue // 使用有界 LinkedBlockingQueue
);
2. ArrayBlockingQueue(有界,需指定容量)
  • 基于数组实现,容量固定,创建时必须指定长度,适合对队列大小有严格限制的场景。
代码语言:javascript
复制
// 容量为 50 的数组队列
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(50);

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60L, TimeUnit.SECONDS,
    queue
);
3. SynchronousQueue(无缓冲,任务直接传递)
  • 队列不存储任务,提交的任务必须立即被线程接收(核心线程或非核心线程),否则会阻塞或触发拒绝策略。
  • 适合任务执行速度快、不需要缓冲的场景(如 Executors.newCachedThreadPool() 底层使用)。
代码语言:javascript
复制
BlockingQueue<Runnable> queue = new SynchronousQueue<>();

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
    queue // 任务需立即被线程处理,无缓冲
);
4、使用任务队列的注意事项
  1. 避免无界队列的 OOM 风险LinkedBlockingQueue 默认无界,若任务提交速度远快于处理速度,队列会无限增长,最终导致内存溢出(OOM)。建议指定队列容量(如 new LinkedBlockingQueue<>(1000))。
  2. 队列长度与线程数的匹配
    • 若任务执行时间短、数量多:适合用较大的队列 + 较少的核心线程(减少线程创建开销)。
    • 若任务执行时间长、数量少:适合用较小的队列 + 较多的最大线程数(避免任务等待过久)。
  3. 与拒绝策略配合:当队列满且线程数达最大值时,需通过拒绝策略(如 AbortPolicyCallerRunsPolicy)处理超额任务,避免任务丢失或系统崩溃。
代码语言:javascript
复制
public class ThreadPoolQueueDemo {
    public static void main(String[] args) {
        // 1. 创建有界任务队列(容量 3)
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(3);

        // 2. 创建线程池(核心 2,最大 4,队列 3)
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2, 4, 60L, TimeUnit.SECONDS,
            queue,
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy() // 队列满时抛出异常
        );

        // 3. 提交 8 个任务(观察队列和线程的协作)
        for (int i = 0; i < 8; i++) {
            int taskId = i;
            executor.submit(() -> {
                try {
                    Thread.sleep(1000); // 模拟任务执行
                    System.out.println("任务 " + taskId + " 执行完毕,线程:" + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println("提交任务 " + taskId + ",当前队列大小:" + queue.size());
        }

        executor.shutdown();
    }
}

使用线程池

一、创建线程池(推荐自定义 ThreadPoolExecutor

线程池的核心实现是 ThreadPoolExecutor,通过 7 个参数可灵活配置,避免 Executors 工具类可能带来的资源风险(如无界队列导致 OOM)。

代码语言:javascript
复制
new ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数(常驻线程,默认不回收)
    int maximumPoolSize,     // 最大线程数(核心 + 非核心线程的上限)
    long keepAliveTime,      // 非核心线程空闲超时时间(超时后回收)
    TimeUnit unit,           // 超时时间单位(如 SECONDS、MILLISECONDS)
    BlockingQueue<Runnable> workQueue, // 任务队列(缓冲待执行的任务)
    ThreadFactory threadFactory,       // 线程工厂(定义线程的创建方式,如名称、优先级)
    RejectedExecutionHandler handler   // 拒绝策略(任务满时的处理方式)
)
二、提交任务(两种方式)

线程池提供两种提交任务的方法,根据是否需要返回值选择:

1. 无返回值任务:execute(Runnable)

适用于只需要执行任务,无需获取结果的场景。

代码语言:javascript
复制
// 提交 Runnable 任务(无返回值)
executor.execute(() -> {
    System.out.println("执行无返回值任务,线程:" + Thread.currentThread().getName());
    // 任务逻辑...
});
2. 有返回值任务:submit(Callable) 或 submit(Runnable)
  • submit(Callable<V>):返回 Future<V>,可通过 get() 获取任务结果。
  • submit(Runnable):返回 Future<?>get() 结果为 null(仅用于判断任务是否完成)
代码语言:javascript
复制
// 提交 Callable 任务(有返回值)
Future<Integer> future = executor.submit(() -> {
    int result = 0;
    for (int i = 1; i <= 100; i++) {
        result += i;
    }
    return result; // 返回计算结果
});

// 获取结果(会阻塞直到任务完成)
try {
    int sum = future.get(); // 获取返回值
    System.out.println("任务结果:" + sum);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace(); // 捕获任务执行中的异常
}
三、关闭线程池(必须操作)

线程池的核心线程默认不会自动销毁,若不关闭,程序会一直运行。需调用以下方法:

代码语言:javascript
复制
// 平缓关闭(推荐)
executor.shutdown();

// 可选:等待线程池关闭(最多等 5 秒)
try {
    if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
        // 若超时,强制关闭剩余任务
        executor.shutdownNow();
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}
四、完整示例:自定义线程池并使用
代码语言:javascript
复制
import java.util.concurrent.*;

public class ThreadPoolUsage {
    public static void main(String[] args) {
        // 1. 配置任务队列(有界队列,容量 5,避免 OOM)
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);

        // 2. 自定义线程工厂(设置线程名称,便于调试)
        ThreadFactory threadFactory = new ThreadFactory() {
            private int threadId = 1;
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("biz-thread-" + threadId++); // 线程名称前缀
                thread.setDaemon(false); // 非守护线程(默认)
                return thread;
            }
        };

        // 3. 定义拒绝策略(队列满时,让提交任务的线程自己执行,缓解压力)
        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();

        // 4. 创建线程池:核心 2 个,最大 4 个,非核心线程空闲 30 秒回收
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2, 4, 30, TimeUnit.SECONDS,
            workQueue, threadFactory, handler
        );

        // 5. 提交 8 个任务(测试线程池的调度逻辑)
        for (int i = 1; i <= 8; i++) {
            int taskId = i;
            // 提交无返回值任务
            executor.execute(() -> {
                try {
                    System.out.println("任务 " + taskId + " 开始,线程:" + Thread.currentThread().getName());
                    Thread.sleep(1000); // 模拟任务耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 6. 关闭线程池
        executor.shutdown();
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 线程池
    • 1. 技术背景脉络
    • 2. 解决方案
    • 2、标准库中的线程池         使用Executors.newFixedThreadPool(10)能创建出固定包含10个线程的线程池。
    • 3、实现线程池         核心操作为submit,将任务加入线程池中
    • 工厂模式有什么作用的?
    • 模拟线程池?
    • shutdown() 和 awaitTermination() 的区别
    • 操作系统中关于进程退出码的约定:
    • 三、总结-保证线程安全的思路
      • 四,常用任务队列类型及用法
  • 使用线程池
    • 一、创建线程池(推荐自定义 ThreadPoolExecutor)
      • 二、提交任务(两种方式)
      • 三、关闭线程池(必须操作)
      • 四、完整示例:自定义线程池并使用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档