13.Java多线程进阶:手动实现线程池与定时器机制详解

目录

一、上节课重点回顾

二、本课重点:手动构造线程池与定时器

[1. 手动构造一个线程池](#1. 手动构造一个线程池)

[1.1 第一个版本:固定线程数目的线程池(基础版)](#1.1 第一个版本:固定线程数目的线程池(基础版))

[1.2 第二个版本:固定线程数目的线程池(完善版)](#1.2 第二个版本:固定线程数目的线程池(完善版))

[1.3 测试自定义线程池](#1.3 测试自定义线程池)

[2. 定时器(Timer)](#2. 定时器(Timer))

[2.1 客户端-服务器模型与定时器](#2.1 客户端-服务器模型与定时器)

[2.2 使用 Timer 类实现定时任务](#2.2 使用 Timer 类实现定时任务)

[2.3 Timer 类内部机制](#2.3 Timer 类内部机制)

三、知识点总结与拓展


在Java并发编程中,线程池和定时器是两个非常核心且实用的组件。本文将详细讲解如何手动构造一个线程池,以及理解Java标准库中的定时器机制。文章会涵盖所有代码示例和关键概念,帮助大家深入理解其底层原理。


一、上节课重点回顾

在深入学习本节课内容之前,我们先回顾一下上节课的核心知识点:

  1. 线程池基础概念

    • 线程相比进程更轻量:线程的创建、销毁和切换开销远小于进程,适合处理高并发任务。

    • 线程池的作用:提前创建好一些线程并保存在池中,当有任务到来时,直接从池中取出线程执行,避免频繁创建和销毁线程带来的性能损耗。

  2. ThreadPoolExecutor 核心参数

    • 重点了解ThreadPoolExecutor的构造函数,其中关键参数包括:

      • corePoolSize:核心线程数。

      • maximumPoolSize:最大线程数。

      • keepAliveTime:非核心线程的空闲存活时间。

      • workQueue:阻塞队列,用于存放待执行的任务。

      • threadFactory:线程工厂,用于创建新线程(工厂模式)。

      • handler:拒绝策略,当线程池和队列都满时的处理方式(拒绝策略)。

  3. Executors 工具类

    • 提供了便捷的工厂方法(如 newFixedThreadPool, newCachedThreadPool)来快速创建不同类型的线程池。

    • 掌握submit(Runnable)方法,用于向线程池提交任务。


二、本课重点:手动构造线程池与定时器

1. 手动构造一个线程池

为了更好地理解线程池的工作原理,我们可以尝试自己动手写一个简易的线程池。

1.1 第一个版本:固定线程数目的线程池(基础版)

这个版本实现了最基本的线程池功能:初始化固定数量的线程,从阻塞队列中取出任务并执行。

java 复制代码
// 固定线程数目的线程池.
class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    // 参数 n 表示线程池的线程数目.
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                try {
                    Runnable task = queue.take();
                    task.run();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }

    // 往线程池中添加新的任务.
    public void submit(Runnable task) throws InterruptedException {
        queue.put(task);
    }
}

代码解析:

  • BlockingQueue<Runnable> queue:使用 LinkedBlockingQueue作为任务队列,它是一个线程安全的阻塞队列。

  • MyThreadPool(int n):构造函数接收线程数量 n,循环创建 n个线程。

  • queue.take():从队列头部取出任务,如果队列为空,则阻塞等待。

  • task.run():执行任务的 run方法。

  • queue.put(task):将新任务放入队列尾部,如果队列已满,则阻塞等待。

1.2 第二个版本:固定线程数目的线程池(完善版)

第一个版本存在一个问题:线程在执行完一个任务后就会退出,没有持续地从队列中获取新任务。我们需要让线程能够循环工作,并且设置线程为守护线程,以便主线程结束时,线程池也能随之结束。

java 复制代码
class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    // 参数 n 表示线程池的线程数目.
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while (true) {
                    try {
                        Runnable task = queue.take();
                        task.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            // 其他前台线程结束,线程池也随着结束.
            t.setDaemon(true);
            t.start();
        }
    }

    // 往线程池中添加新的任务.
    public void submit(Runnable task) throws InterruptedException {
        queue.put(task);
    }
}

改进点:

  • while (true):让线程在 take()run()之间循环,不断从队列中获取新任务。

  • t.setDaemon(true):将线程设置为守护线程。这意味着当所有前台线程(如 main线程)结束时,JVM 会自动终止这些守护线程,从而优雅地关闭线程池。

1.3 测试自定义线程池

我们编写一个 Demo32类来测试上面实现的 MyThreadPool

java 复制代码
public class Demo32 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(4); // 创建4个线程的线程池
        for (int i = 0; i < 1000; i++) {
            int id = i;
            pool.submit(() -> {
                Thread cur = Thread.currentThread();
                System.out.println("hello " + cur.getName() + ", " + id);
            });
        }
        // 加 sleep 确保线程池的线程有足够的时间执行完任务.
        Thread.sleep(1000);
    }
}

测试说明:

  • 创建拥有 4 个线程的线程池。

  • 循环提交 1000 个任务,每个任务打印当前线程名和任务 ID。

  • 最后主线程 sleep(1000),确保有足够的时间让线程池中的线程执行完所有任务。


2. 定时器(Timer)

定时器用于在未来某个时间点执行任务,或者在固定的时间间隔内重复执行任务。Java 标准库中提供了 Timer类来实现这一功能。

2.1 客户端-服务器模型与定时器

一个简单的客户端-服务器交互图:

  • 客户端发出请求后,需要等待服务器的响应回来。

  • 网络 传输数据,服务器处理请求并返回响应。

  • 定时器在这里可以理解为一种"闹钟",用于在指定时间触发某些操作,比如定时检查服务器状态、定时发送心跳包等。

2.2 使用 Timer 类实现定时任务

java 复制代码
public class Demo33 {
    public static void main(String[] args) {
        Timer timer = new Timer();

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer 3000");
            }
        }, 3000); // 延迟3000毫秒(3秒)后执行

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer 2000");
            }
        }, 2000); // 延迟2000毫秒(2秒)后执行

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer 1000");
            }
        }, 1000); // 延迟1000毫秒(1秒)后执行

        // 主线程睡眠4秒,确保定时器任务有足够时间执行
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 取消定时器,停止所有后续任务
        timer.cancel();
    }
}

代码解析:

  • Timer timer = new Timer();:创建一个定时器实例。这个类内部有专门的线程来执行任务,且内置的线程是前台线程。

  • timer.schedule(TimerTask task, long delay):安排一个 TimerTask在指定的延迟时间后执行。

  • TimerTask:这是一个抽象类,实现了 Runnable接口,我们需要重写其 run()方法来定义具体的任务。

  • timer.cancel():取消定时器,释放相关资源。

执行顺序:

程序启动后,会按照延迟时间的长短依次执行任务:

  1. 1秒后:输出 hello timer 1000

  2. 2秒后:输出 hello timer 2000

  3. 3秒后:输出 hello timer 3000

2.3 Timer 类内部机制

  • Timer类内部维护了一个任务队列(通常是优先队列),用于存储所有已安排但未执行的 TimerTask

  • 它有一个专门的后台线程(注意:实际 Java 源码中 Timer使用的是 TimerThread,它继承自 Thread,默认是前台线程),这个线程会不断地从队列中取出任务,检查是否到达执行时间,如果到达则执行。

  • timer.schedule(...):安排任务,将任务加入队列。

  • TimerTaskpublic abstract class TimerTask implements Runnable,这是所有定时任务的基类。

重要提示:

强调Timer的后台线程负责执行所有任务,主线程只需要安排任务即可,无需等待。


三、知识点总结与拓展

通过本节课的学习,我们不仅掌握了如何手动实现一个简单的线程池,还了解了 Java 标准库中的定时器机制。以下是核心知识点的总结:

  1. 线程池的核心思想:复用线程、减少创建/销毁开销、任务排队。

  2. 阻塞队列的作用:在多线程环境下安全地进行生产者-消费者模式的任务传递。

  3. 守护线程的意义:让线程池能够随着主线程的结束而自动关闭,避免程序无法正常退出。

  4. 定时器的作用:在指定时间或间隔执行任务,常用于定时任务调度。

  5. Timer 的内部机制:基于一个优先队列和一个专用线程,按时间顺序执行任务。

拓展思考:

  • 在实际项目中,我们通常会使用 ExecutorService(如 ThreadPoolExecutor)而不是自己造轮子,因为它功能更强大、更健壮。

  • Timer有一些局限性,比如如果一个任务执行时间过长,会影响后续任务的准时执行。在需要更精确、更灵活的定时任务调度时,可以考虑使用 ScheduledExecutorService或第三方库如 Quartz等

相关推荐
弹简特1 小时前
【Java项目-轻聊】10-实现会话管理模块
java·开发语言·数据库
人道领域1 小时前
Java后端开发者转型AIAgent开发路线指南
java·开发语言
uhakadotcom1 小时前
结合着 fastapi 使用,anyio 通常可以如何使用 , 它和 uvloop 在性能上有啥差异
后端·面试·github
许彰午1 小时前
35_Java设计模式之工厂模式
java·开发语言·设计模式
凡人叶枫1 小时前
Effective C++ 条款32:确定你的 public 继承塑模出 is-a(是一种)关系
java·linux·开发语言·c++·嵌入式开发
码云骑士1 小时前
18-生成器不只是省内存(上)-yield的状态机模型与帧暂停
c语言·开发语言·python
我喜欢就喜欢1 小时前
C++ 连接 Ollama 本地大模型:从原生 HTTP 调用到高性能封装实践
开发语言·c++·http
光影少年2 小时前
避免不必要渲染:PureComponent、memo、useMemo、useCallback
react.js·面试·掘金·金石计划
踏着七彩祥云的小丑2 小时前
Go学习第8天:接口 + 泛型 + 错误处理
开发语言·学习·golang·go