首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【JavaSE】线程池 && 定时器详解

【JavaSE】线程池 && 定时器详解

原创
作者头像
lirendada
发布2026-04-08 19:56:04
发布2026-04-08 19:56:04
670
举报
文章被收录于专栏:JavaJava

线程池

一、Java 线程池总体架构

Java 线程池的核心接口是 ExecutorExecutorService,最常用的实现类是 ThreadPoolExecutor

并且 Java 还帮我们搞了一个工厂类 Executors,用于创建特定需求的 ThreadPoolExecutor,省去了麻烦的构造传参过程。

它们的关系如下图所示:

为什么常用 ExecutorService 而不用 ThreadPoolExecutor 直接接收线程池对象❓❓❓ ① 面向接口编程的思想

  • 灵活与扩展性:ExecutorService 是接口,ThreadPoolExecutor 是实现类。接口能屏蔽实现细节,提高代码的灵活性。比如以后你想换成另一个线程池实现,比如ScheduledThreadPoolExecutor,只用改一行初始化、不用大改其他代码。
  • 更好的解耦:依赖接口而不是实现类,可以让你的代码更松耦合。测试、替换实现、未来新实现(比如自定义线程池)都更容易。

② 常用的工厂方法都返回 ExecutorService 常见的线程池创建方法返回的本来就是 ExecutorService 接口:

这样别人用你的代码/方法,只要依赖 ExecutorServiceAPI 就行,不用知道是不是ThreadPoolExecutor,还是别的啥池子。 ③ 实际开发只用到 ExecutorService 的方法就够了

  • 线程池的绝大多数常用操作,比如 execute()submit()shutdown() 等等,全部定义在 ExecutorService 接口里。日常 99% 的场景都不需要依赖实现类的特有方法。
  • 只有极少数高级场景(比如你要去读线程池的活动线程数 activeCount,或者定制特殊的配置参数),才可能需要用到实现类 ThreadPoolExecutor 的专有方法。

二、快速上手

① 使用 Executors 创建线程池

代码语言:javascript
复制
public static void main(String[] args) {
    // 1. 固定大小线程池:适用于负载稳定的场景
    ExecutorService p1 = Executors.newFixedThreadPool(10);

    // 2. 可缓存线程池:有任务就创建新线程,空闲线程会回收,适用于大量短生命周期的任务
    ExecutorService p2 = Executors.newCachedThreadPool();

    // 3. 单线程的线程池,保证任务顺序执行
    ExecutorService p3 = Executors.newSingleThreadExecutor();

    // 4. 定时/周期性线程池:适用于延迟或周期性任务调度
    ExecutorService p5 = Executors.newScheduledThreadPool(10);
}

线程池与阻塞队列的匹配关系如下表所示:

线程池类型

使用的阻塞队列

设计目标

FixedThreadPool

LinkedBlockingQueue

固定线程数,任务队列无界,避免线程频繁创建

CachedThreadPool

SynchronousQueue

线程数动态扩展,任务直接传递,适合短时任务

SingleThreadExecutor

LinkedBlockingQueue

单线程顺序执行任务

ScheduledThreadPool

DelayedWorkQueue

延迟或周期性任务调度

要手动创建线程池的话,可以看后面 ThreadPoolExecutor 参数的解释!

② 提交任务到线程池

创建出线程池之后,就要提交 "任务" 到线程池中,由线程池自己调度或创建新线程来完成该 "任务"。下面是 Executor 中给出的两个提交任务的接口:

execute

submit(推荐⭐⭐⭐)

接收参数类型

Runnable(无返回值)

Runnable | Callable

返回值

void(无返回)

Future(可获得返回和异常)

任务异常处理

线程池会捕获但不会报告异常

Future.get() 时抛出 ExecutionException

任务中断支持

可以 cancel 中断任务

用途

简单任务,不关心结果

需要关心任务结果或异常

现代最佳实践其实推荐多用 submit,因为哪怕暂时不需要返回值,有 Future 也能后续扩展,比如取消、结果统计等。

代码语言:javascript
复制
// 1. 创建一个指定数量线程的线程池
ExecutorService p1 = Executors.newFixedThreadPool(10);

// 2. 让线程池中10个线程来处理100个打印任务
for(int i = 0; i < 100; ++i) {
    int index = i;
    p1.submit(() -> { // submit不处理返回值是没问题的!
        System.out.println(Thread.currentThread().getName() + ":" + index);
    });
}

③ 关闭线程池

shutdown() 优雅关闭,不再接收新任务,等待现有任务执行完毕。

代码语言:javascript
复制
p1.shutdown();

三、ThreadPoolExecutor 参数的理解💥💥💥

代码语言:javascript
复制
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    int corePoolSize,                    // 核心线程数
    int maximumPoolSize,                 // 最大线程数
    long keepAliveTime,                  // 非核心闲置线程最大存活时间
    TimeUnit unit,                       // keepAliveTime的时间单位
    BlockingQueue<Runnable> workQueue,   // 任务队列
    ThreadFactory threadFactory,         // 线程工厂
    RejectedExecutionHandler handler     // 拒绝策略
);
  • corePoolSize核心线程数量(不会被回收,相当于正式员工,一旦录用,永不辞退)
  • maximumPoolSize最大线程数量(相当于正式员工 + 临时工的数量,当临时工那部分闲置超过了 keepAliveTime,就会被炒掉)
    • 当最大活动线程数量超过 maximumPoolSize,新任务就会触发下面的 "拒绝策略"
  • keepAliveTime非核心闲置线程最大存活时间(即临时工允许的闲置时间)
    • 如果设置了 allowCoreThreadTimeOut(true),那么核心线程如果也空闲时间超过该值也会被回收。
  • unitkeepAliveTime的时间单位(纳秒、微妙、毫秒、秒、分钟、小时、天)
  • workQueue线程池内部存储待执行任务的阻塞队列
  • threadFactory创建线程的工厂类,控制创建线程的细节(如线程命名、优先级、是否为守护线程等)
    • Java 自带了现成的创建线程的工厂类,最常用的就是 Executors.defaultThreadFactory()
    • 只有在有特殊需求时才需要自定义,比如:
      • 想让线程名带有特定业务标识、更好排查问题
      • 希望线程变成 "守护线程"
      • 想统一捕获线程内部未捕获异常
      • 对线程优先级有特殊要求
      • 希望在线程创建时做监控或自定义初始化
  • handler拒绝策略,就是任务太多,超过 workQueue 的容量后,要怎么处理,有四种内置选项:
    • AbortPolicy(默认):直接抛出 RejectedExecutionException 异常
    • DiscardPolicy:直接丢弃
    • DiscardOldestPolicy:丢弃队列里最老的任务,然后再尝试入队当前任务
    • CallerRunsPolicy:由提交任务的线程自己执行该任务
代码语言:javascript
复制
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    5,                                   // corePoolSize
    10,                                  // maximumPoolSize
    60,                                  // keepAliveTime
    TimeUnit.SECONDS,                    // 单位
    new ArrayBlockingQueue<>(100),       // 有界队列
    Executors.defaultThreadFactory(),    // 默认线程工厂
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

// 如果要手动自定义线程工厂,可以按照下面这样子处理,然后传myFactory给ThreadPoolExecutor即可
AtomicInteger threadId = new AtomicInteger(1);
ThreadFactory myFactory = r -> {
    Thread t = new Thread(r, "mythread-" + threadId.getAndIncrement());
    t.setDaemon(true); // 设置线程为守护线程
    return t;
};

💥注意事项:

  • 参数中 BlockingQueue<Runnable> 中的 RunnableThreadFactory.newThread(Runnable r) 中的 Runnable 是不同的,区别如下所示:
    • ThreadFactory.newThread(Runnable r) 中的 r
      1. 这个 Runnable r 不是你的 "具体业务",而是线程池 "工作线程" 的调度骨架Worker)。它的职责就是反复从任务队列 BlockingQueue<Runnable> 中取任务出来执行,一旦取到任务就执行任务的 run()
      2. 换句话说,这个 Runnable r 更像是 "消费者行为的实现",并不是你自己提交的具体业务任务。
    • 阻塞队列或者 submit(Runnable r) 中的 r
      1. 这才是你写的 "具体业务":比如下载文件、处理订单、统计数据……
      2. 需要注意的是 submit(Runnable r) 中的 r 和阻塞队列 BlockingQueue<Runnable> 中的 Runnable一回事,都是指具体业务,阻塞队列相当于提供了一个存放 submit 提交业务的空间
      3. 它被丢进线程池的任务队列,等消费者(即线程池 Worker 线程)来拿走处理。
    • 总结:submit(Runnable r) 中的 r 是 "具体业务",这些业务会被当作 "原料" 投向线程池由工作线程拿去处理,而工作线程的属性等是由 ThreadFactory.newThread(Runnable r) 中的 r 提供的,并且这些工作线程由线程池自主调度,不需要程序员手动处理
代码语言:javascript
复制
任务提交
    │
    ├─ 核心线程未满 → 创建核心线程执行
    │
    ├─ 核心线程满 → 队列未满 → 放入队列等待
    │
    └─ 队列满 → 非核心线程未满 → 创建非核心线程执行
          │
          └─ 非核心线程满 → 拒绝策略
  • allowCoreThreadTimeOut 可以让核心线程也根据空闲时间销毁
  • 队列类型影响策略
    • 有界队列(ArrayBlockingQueue) → 队列满才创建非核心线程
    • 无界队列(LinkedBlockingQueue) → 队列几乎不会满 → 非核心线程几乎不创建

四、自主实现简易线程池(理解原理、细节即可)

实现一个简易的线程池,是为了帮助理解线程池中阻塞队列的作用、线程的执行这些环节,实现起来并不难,只需要用一个阻塞队列,然后启动线程执行阻塞队列中的任务即可!

代码语言:javascript
复制
public class MyThreadPool {
    private BlockingQueue<Runnable> qe = new LinkedBlockingQueue<>();

    public MyThreadPool(int n) {
        for(int i = 0; i < n; ++i) {
            Thread thread = new Thread(() -> {
                // 让每个线程循环处理阻塞队列中的业务,然后执行
                while (true) {
                    try {
                        Runnable r = qe.take();
                        r.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            thread.start(); // 别忘了启动线程
        }
    }
    
    // 将具体业务插入到阻塞队列中,具体调度由阻塞队列自己处理
    public void submit(Runnable r) {
        try {
            qe.put(r);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

测试代码非常简单,提交任务让线程池去处理即可,如下所示:

代码语言:javascript
复制
public static void main(String[] args) throws InterruptedException {
    MyThreadPool mtp = new MyThreadPool(5);
    for(int i = 0; i < 100; ++i) {
        int index = i;
        mtp.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "处理" + index + "号业务");
        });
    }
}

定时器

定时器是软件开发中的一个重要组件,就是程序中闹钟,时间到了就会执行某段提前设定好的代码。比如网络通信中的最大未响应时间,超过一段时间没有收到数据,此时程序应该尝试发起重新连接等等情况。

一、标准库中的定时器 -- Timer

TimerJava 中提供的一种简单的定时任务调度工具类,用于安排一个任务在指定的时间执行一次,或者周期性地执行。

Timer 的接口如下表所示:

方法签名

说明

是否周期执行

延迟类型

schedule(TimerTask task, long delay)

延迟 delay 毫秒后执行一次任务

固定延迟

schedule(TimerTask task, Date time)

在指定时间点 time 执行一次任务

固定时间点

schedule(TimerTask task, long delay, long period)

延迟 delay 毫秒后开始,每隔 period 毫秒执行一次任务

固定延迟(上次任务结束后再延迟)

schedule(TimerTask task, Date firstTime, long period)

指定首次时间 firstTime,之后每隔 period 毫秒执行

固定延迟

scheduleAtFixedRate(TimerTask task, long delay, long period)

延迟 delay 毫秒后开始,每隔 period 毫秒强制执行一次(不管上次是否完成)

固定频率(时间点为准)

scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

指定首次时间点 firstTime,之后按固定频率周期执行

固定频率

cancel()

取消当前定时器中所有已安排的任务

purge()

清除已被取消的任务(返回清除的个数)

💥注意事项:

  • 固定延迟:下一个任务从上一个任务执行完毕后开始计算延迟,不会并发,适合非精确定时。
  • 固定频率:下一个任务的开始时间是按理想周期时间表推进,即使上一个没执行完也尝试补上,可能并发,适合高精度周期调度。

在使用的时候,Timer 需要配合 TimerTask 一起使用,如下所示:

代码语言:javascript
复制
// 1. 定义Timer对象
Timer timer = new Timer();

// 2. 调用schedule设置定时任务TimerTask,以及定时时长,其中TimerTask要重写run()
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("定时器执行3000ms");
    }
}, 3000);

其中 TimerTask 实现了 Runnable 接口,本质上就是在 Runnable 的基础上增加了一些属性、方法等,所以定义 TimerTask 时候重写一下其中的 run() 即可

二、自主实现定时器(理解原理、细节即可)

代码语言:javascript
复制
// 任务类,保存任务以及任务执行时间
class Task implements Comparable<Task> {
    private Runnable task;
    private long time; // 为了方便判断时间是否到达, 所以保存绝对的时间戳

    public Task(Runnable task, long time) {
        this.task = task;
        this.time = System.currentTimeMillis() + time; // 注意这里存放的是时间戳,所以要计算一下
    }

    public Runnable getTask() {
        return task;
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(Task o) {
        return (int)(this.time - o.time);
    }
}

public class MyTimer {
    // 使用优先级队列作为存放定时任务的容器,且要以时间间隔最小的任务作为堆顶
    private PriorityQueue<Task> qe = new PriorityQueue<>();

    // 队列涉及到多线程操作,需要进行加锁
    // 并且为了避免“忙等”,可以用wait()和notify()配合,来让出CPU资源
    private final Object locker = new Object();

    public MyTimer() {
        // 构建线程去处理定时任务
        Thread thread = new Thread(() -> {
            while(true) {
                // 加锁
                try {
                    synchronized(locker) {
                        // 判断是否有定时任务
                        if(qe.isEmpty() == true) {
                            // 1. 直接continue会导致忙等,浪费cpu资源
                            locker.wait();
                        }

                        // 走到这说明存在定时任务,则判断是否到达执行时间
                        Task t = qe.peek();
                        if(t.getTime() > System.currentTimeMillis()) {
                            // 2. 时间还没到,直接continue同样会造成忙等,浪费cpu资源,所以这里同样使用wait()等待唤醒
                            //   不同的是这里要设置超时时间,因为在wait()阻塞到该定时任务时间到之前如果没有新的任务来的话,这个线程
                            //   都不会被唤醒,导致任务没有及时处理,所以要设置超时时间为剩余等待时间!
                            long gap = t.getTime() - System.currentTimeMillis();
                            locker.wait(gap);
                        } else {
                            // 时间到了,执行任务
                            t.getTask().run();
                            qe.poll();
                        }
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
    }

    // 插入定时任务
    public void schedule(Runnable r, long delay) {
        synchronized (locker) {
            qe.offer(new Task(r, delay));
            locker.notify();
        }
    }
}

💥注意事项:

  1. 标准库里面有一个现成的阻塞优先级队列 BlockingPriorityQueue,但这里不用它的原因是因为 BlockingPriorityQueue 没有定时阻塞的功能,在构造方法中判断是否到达执行任务的时间,没办法进行定时阻塞,从而没办法实现该逻辑,所以只能用 PriorityQueue 加上 synchronized 来实现!
  2. 一旦程序中涉及到加锁的地方存在条件判断的时候,要考虑清楚当条件不符合时候会不会出现形如 "忙等" 的情况,然后分析这种 "忙等" 的情况对程序带来的效率问题大不大,大的话最好要wait()notify() 配合组合和唤醒,避免 CPU 资源浪费

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 线程池
    • 一、Java 线程池总体架构
    • 二、快速上手
      • ① 使用 Executors 创建线程池
      • ② 提交任务到线程池
      • ③ 关闭线程池
    • 三、ThreadPoolExecutor 参数的理解💥💥💥
    • 四、自主实现简易线程池(理解原理、细节即可)
  • 定时器
    • 一、标准库中的定时器 -- Timer
    • 二、自主实现定时器(理解原理、细节即可)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档