目录
[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标准库中的定时器机制。文章会涵盖所有代码示例和关键概念,帮助大家深入理解其底层原理。
一、上节课重点回顾
在深入学习本节课内容之前,我们先回顾一下上节课的核心知识点:
-
线程池基础概念
-
线程相比进程更轻量:线程的创建、销毁和切换开销远小于进程,适合处理高并发任务。
-
线程池的作用:提前创建好一些线程并保存在池中,当有任务到来时,直接从池中取出线程执行,避免频繁创建和销毁线程带来的性能损耗。
-
-
ThreadPoolExecutor 核心参数
-
重点了解
ThreadPoolExecutor的构造函数,其中关键参数包括:-
corePoolSize:核心线程数。 -
maximumPoolSize:最大线程数。 -
keepAliveTime:非核心线程的空闲存活时间。 -
workQueue:阻塞队列,用于存放待执行的任务。 -
threadFactory:线程工厂,用于创建新线程(工厂模式)。 -
handler:拒绝策略,当线程池和队列都满时的处理方式(拒绝策略)。
-
-
-
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秒后:输出
hello timer 1000 -
2秒后:输出
hello timer 2000 -
3秒后:输出
hello timer 3000
2.3 Timer 类内部机制
-
Timer类内部维护了一个任务队列(通常是优先队列),用于存储所有已安排但未执行的TimerTask。 -
它有一个专门的后台线程(注意:实际 Java 源码中
Timer使用的是TimerThread,它继承自Thread,默认是前台线程),这个线程会不断地从队列中取出任务,检查是否到达执行时间,如果到达则执行。 -
timer.schedule(...):安排任务,将任务加入队列。 -
TimerTask:public abstract class TimerTask implements Runnable,这是所有定时任务的基类。
重要提示:
强调Timer的后台线程负责执行所有任务,主线程只需要安排任务即可,无需等待。
三、知识点总结与拓展
通过本节课的学习,我们不仅掌握了如何手动实现一个简单的线程池,还了解了 Java 标准库中的定时器机制。以下是核心知识点的总结:
-
线程池的核心思想:复用线程、减少创建/销毁开销、任务排队。
-
阻塞队列的作用:在多线程环境下安全地进行生产者-消费者模式的任务传递。
-
守护线程的意义:让线程池能够随着主线程的结束而自动关闭,避免程序无法正常退出。
-
定时器的作用:在指定时间或间隔执行任务,常用于定时任务调度。
-
Timer 的内部机制:基于一个优先队列和一个专用线程,按时间顺序执行任务。
拓展思考:
-
在实际项目中,我们通常会使用
ExecutorService(如ThreadPoolExecutor)而不是自己造轮子,因为它功能更强大、更健壮。 -
Timer有一些局限性,比如如果一个任务执行时间过长,会影响后续任务的准时执行。在需要更精确、更灵活的定时任务调度时,可以考虑使用ScheduledExecutorService或第三方库如 Quartz等