Java EE初阶——定时器和线程池
3. 定时器
定时器(Timer) 是一种用于在指定时间间隔后执行任务或周期性执行任务的工具。它广泛应用于定时任务、超时控制、周期性操作等场景。
Java 提供了多种定时器实现方式,适用于不同场景:
1. Timer
和 TimerTask
(传统方式)
- 核心类:
Timer
:定时器核心类,Timer 核⼼⽅法为 schedule ,负责调度任务。TimerTask
:抽象类,代表可调度的任务,需重写run()
方法。
import java.util.Timer;
import java.util.TimerTask;public class Test {public static void main(String[] args) throws InterruptedException {//创建 Timer 实例Timer timer = new Timer();// 延迟 2 秒执行任务1timer.schedule(new TimerTask(){//创建了一个TimerTask实例//重新 run 方法,@Overridepublic void run(){System.out.println("任务");}},2000);// Thread.sleep(2000);// timer.cancel();//终止定时器}
}
schedule(TimerTask task, long delay)
。
new Timer.schedule()
方法用于安排任务的执行。第一个参数是一个
TimerTask
对象,通过匿名内部类的方式创建了一个TimerTask
实例,表示任务,并重写了run()
方法。
在
run()
方法中,打印了当前时间(以毫秒为单位,从1970年1月1日00:00:00 GMT开始计算)。第二个参数是延迟时间,单位为毫秒。定时器内部有一个线程,到延迟时间后就会执行任务。
执行流程:
程序启动,创建
Timer
实例调用
schedule
方法安排任务
Timer
内部的后台线程开始计时2秒后,
Timer
的后台线程调用TimerTask
的run
方法
注意事项:
Timer
创建的线程默认是非守护线程(用户线程),即使main
方法结束,程序也不会退出,除非调用timer.cancel()
或所有非守护线程都结束。如果需要在任务执行后让程序退出,可以:
调用
timer.cancel()
取消定时器或者将定时器设置为守护线程:
Timer timer = new Timer(true);
如果
run
方法中抛出未捕获的异常,定时器的执行线程会终止,但不会影响其他线程。
- 特点:
- 单线程调度:所有任务由同一个线程按顺序执行,任务执行延迟会影响后续任务。
- 简单易用:适合轻量级定时任务。
1. Timer的schedule方法
方法 | 说明 |
---|---|
schedule(TimerTask task, long delay) | 延迟指定时间后执行任务 |
schedule(TimerTask task, Date time) | 在指定时间执行任务 |
schedule(TimerTask task, long delay, long period) | 延迟指定时间后开始,以固定间隔重复执行 |
schedule(TimerTask task, Date firstTime, long period) | 从指定时间开始,以固定间隔重复执行 |
2. cancel()方法
cancel()
方法用于 终止定时器并取消所有已安排但尚未执行的任务。
-
终止
Timer
的后台线程:调用后,Timer
的调度线程会被终止,不再执行任何任务。 -
取消所有待执行的任务:所有已通过
schedule()
或scheduleAtFixedRate()
安排但尚未执行的任务都会被取消。 -
不会影响正在执行的任务:如果某个任务正在执行,
cancel()
不会中断它,但后续任务不会再被执行。
注意事项:
cancel()
只能调用一次:
如果多次调用
cancel()
,后续调用不会有任何效果。调用后,
Timer
对象不能再安排新任务(会抛出IllegalStateException
)。
Timer
线程不会立即终止:
cancel()
只是标记Timer
为已取消,正在执行的任务(如果有)会继续完成。如果希望立即停止正在运行的任务,可以使用
Thread.interrupt()
机制(但TimerTask
本身不支持中断)。具有不可逆性
1. 调用
cancel()
后,定时器无法重新启动。若需继续调度任务,必须创建新 的Timer
实例。4. 线程终止
- 若
Timer
线程是 JVM 中唯一的非守护线程,调用cancel()
后,JVM 可能退出。- 若
Timer
是守护线程(默认),则仅释放线程资源,不影响 JVM 运行。
场景 1:Timer 线程正在执行任务时终止
- 操作:调用
timer.cancel()
。- 结果:
- 正在执行的任务会继续执行完毕,除非任务中响应中断(例如捕获
InterruptedException
并提前终止)。- 未执行的任务会被取消,不再执行。
场景 2:Timer 线程处于阻塞状态时终止
- 操作:调用
timer.cancel()
。- 结果:
cancel()
会向 Timer 线程发送中断(interrupt()
),尝试唤醒阻塞的线程。- 如果线程因
sleep()
、wait()
等可中断阻塞被中断,会抛出InterruptedException
,任务会提前终止,未执行的任务也会被取消。- 如果线程因不可中断的阻塞(如同步锁竞争)被阻塞,
interrupt()
不会立即生效,线程会继续阻塞,直到阻塞解除后才会检测到中断状态,此时任务可能继续执行或提前终止(取决于代码是否处理中断)。
-
替代方案(推荐):
-
Timer
是早期 API,存在单线程执行、异常影响调度等问题。 -
现代 Java 推荐使用
ScheduledThreadPoolExecutor
(支持线程池、更灵活的任务控制)。
-
3. 定时器的实现
每个线程都带有 dalay 时间,队首元素放时间小的,检查队首时间是否到时间即可
java 标准库中提供了PriorityBorinkingQueue(线程安全)和PriorityQueue(线程不安全)手动加锁控制
定时器的构成
• ⼀个带优先级队列(不要使⽤ PriorityBlockingQueue, 容易死锁!)
• 队列中的每个元素是⼀个 Task 对象.
• Task 中带有⼀个时间属性, 队⾸元素就是即将要执⾏的任务
• 同时有⼀个 t 线程⼀直扫描队⾸元素, 看队⾸元素是否需要执⾏
import java.util.PriorityQueue;
//定时任务类,表示一个待执行的任务
class MyTimerTask implements Comparable<MyTimerTask>{private long time;//执行任务时间,ms级时间戳private Runnable runnable;//记录任务要执行的代码/*** 构造函数* @param runnable 要执行的任务* @param delay 延迟时间(毫秒)*/public MyTimerTask(Runnable runnable,long delay){if(runnable == null){throw new NullPointerException("任务为空");}if (delay < 0)throw new IllegalArgumentException("延迟时间不可能为负数");this.runnable = runnable;time = System.currentTimeMillis()+delay;//绝对执行时间}//执行任务public void run(){runnable.run();}//获取执行任务时间public long getTime(){return time;}//比较方法,用于优先级队列排序public int compareTo(MyTimerTask o1){return Long.compare(this.time , o1.time);}
}
//自定义定时器类
class MyTimer{private Thread t = null;// 工作线程private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();// 任务队列private Object object = new Object();//创建锁对象private volatile boolean isRunning = true;//定时器运行状态标志//终止定时器public void cancel(){isRunning = false;queue.clear();// 清空任务队列//结束t线程t.interrupt();}//任务调度public void schedule(Runnable runnable,long delay){synchronized (object){if(!isRunning){throw new IllegalStateException("定时器已关闭");}MyTimerTask task = new MyTimerTask(runnable,delay);queue.offer(task);object.notify();//唤醒}}// 构造方法,创建扫描线程,让扫描线程来完成判定和执行public MyTimer(){t = new Thread(()->{while (isRunning){try{synchronized (object){while (isRunning && queue.isEmpty()){//队列空,阻塞object.wait();}if(!isRunning){break;// 定时器已关闭}//获取头任务MyTimerTask task = queue.peek();while (task.getTime() <= System.currentTimeMillis()){//时间未到,阻塞//阻塞delay 时间后,自动唤醒object.wait(task.getTime()-System.currentTimeMillis());}else{queue.poll();task.run();}}}catch (InterruptedException e){// 被中断时,检查是否是因cancel()调用if (!isRunning) {System.out.println("定时器工作线程正常退出");break; // 正常终止}// 如果是意外中断,恢复中断状态Thread.currentThread().interrupt();System.err.println("工作线程被意外中断");break;}}});t.start();}
}
public class Test3 {public static void main(String[] args) throws InterruptedException {MyTimer timer = new MyTimer();timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("任务1");}},2000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("任务2");}},1000);Thread.sleep(4000);timer.cancel();}
}
4. 线程池
如果频繁创建和销毁线程开销是非常大的
1. 轻量级线程(纤程/协程)
java 21 引入 虚拟线程(Virtual Threads)
协程本质,是程序员在用户态中进行调度,不是靠内核中的调度器调度的
虚拟线程由 JVM 管理,依附于底层的操作系统线程(载体线程),创建成本极低(约 KB 级别)。
2. 线程池(Thread Pool) 是一种多线程处理模式,通过提前创建并管理一组线程,避免频繁创建和销毁线程的开销,从而提高系统性能和资源利用率。
1. 核心原理
- 线程复用:线程池创建后,线程不会立即销毁,而是重复执行任务。
- 任务队列:当线程池中的线程都在繁忙时,新任务会被暂存到队列中,等待线程空闲。
- 动态管理:根据负载自动调整线程数量,避免资源浪费。
2. 优势
- 降低资源消耗:减少线程创建 / 销毁的开销(每个线程创建约需消耗 1MB 栈内存)。
- 提高响应速度:任务到达时无需等待线程创建,直接由空闲线程处理。
- 控制并发数量:通过设置线程池大小,避免因线程过多导致的内存溢出或 CPU 过度切换。
- 统一管理:提供线程监控、异常处理等机制,便于系统调优
public ThreadPoolExecutor(int corePoolSize, // 核心线程数(最小线程数,即使空闲也不销毁)int maximumPoolSize, // 最大线程数(核心线程+临时线程的总数上限)long keepAliveTime, // 临时线程的存活时间(当线程数 > 核心数时,空闲线程的最大存活时间)TimeUnit unit, // 存活时间单位BlockingQueue<Runnable> workQueue, // 任务队列(存储待执行的任务)ThreadFactory threadFactory, // 线程工厂(自定义线程创建逻辑)RejectedExecutionHandler handler // 拒绝策略(任务队列满且线程数达上限时的处理方式)
)
参数 | 描述 |
---|---|
corePoolSize | 核心线程数,线程池的 “常驻线程”,默认情况下会一直存活(除非设置 allowCoreThreadTimeOut )。 |
maximumPoolSize | 线程池能创建的最大线程数,核心线程 + 临时线程的总数不能超过该值。 |
keepAliveTime | 临时线程(超过核心线程数的部分)的空闲存活时间,超时后会被销毁。 |
workQueue | 待执行任务队列,常用类型包括: - ArrayBlockingQueue (有界队列)- LinkedBlockingQueue (无界队列)- SynchronousQueue (直接移交队列) |
threadFactory | 用于创建线程,可自定义线程名称、守护线程等属性,便于调试。 |
handler | 拒绝策略,常用策略包括: - AbortPolicy (抛出异常,默认)- CallerRunsPolicy (任务由调用者线程处理)- DiscardPolicy (丢弃新任务)- DiscardOldestPolicy (丢弃队列中最旧的任务) |
1. 工厂模式
定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。
工厂模式的核心角色
- 抽象产品(Product)
定义所有具体产品的公共接口或抽象类,约束产品的行为。 - 具体产品(Concrete Product)
实现抽象产品接口,是实际创建的对象。 - 抽象工厂(Factory)
定义创建产品的公共接口(如createProduct()
方法)。 - 具体工厂(Concrete Factory)
实现抽象工厂接口,负责创建具体产品对象。
Executors
是 Java 并发包 (java.util.concurrent
) 中提供的一个线程池工厂类,它封装了 ThreadPoolExecutor
的复杂配置过程,提供了一系列静态工厂方法来创建不同类型的线程池。
ThreadPoolExecutor
构造函数参数较多,配置复杂,Executors
通过预定义配置简化了线程池的创建过程,使开发者能够快速获得适合常见场景的线程池。
1. Executors 工厂类核心方法
1. newFixedThreadPool(int nThreads)
- 作用:创建固定大小的线程池,核心线程数和最大线程数相等(
corePoolSize = maxPoolSize = nThreads
)。 - 队列类型:使用 无界队列
LinkedBlockingQueue
(容量为Integer.MAX_VALUE
)。
- 适用场景:
适用于已知并发量、任务耗时均匀的场景(如数据库连接池),但高负载下可能导致 OOM(无界队列积压大量任务)。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Test8 {public static void main(String[] args) {ExecutorService pool = Executors.newFixedThreadPool(2);for(int i=0;i<1000;i++){int n = i;pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务"+n+",当前线程:"+Thread.currentThread().getName());}});}}
}
2. newCachedThreadPool()
- 作用:创建可缓存的线程池,核心线程数为 0,最大线程数为
Integer.MAX_VALUE
,空闲线程存活时间为 60 秒。 - 队列类型:使用 直接移交队列
SynchronousQueue
(不存储任务,直接传递给线程)。
- 适用场景:
适合处理大量短时间任务(如 HTTP 请求),但高并发下可能创建海量线程导致系统崩溃。
3. newSingleThreadExecutor()
- 作用:创建单线程池,核心线程数和最大线程数均为 1,保证任务按顺序执行。
- 队列类型:使用 无界队列
LinkedBlockingQueue
。
- 适用场景:
需要保证任务顺序执行、单线程处理的场景(如日志序列化写入)。
4. newScheduledThreadPool(int corePoolSize)
- 作用:创建支持定时 / 周期性任务的线程池,核心线程数由参数指定,最大线程数为
Integer.MAX_VALUE
。 - 队列类型:使用 延迟队列
DelayedWorkQueue
。
ScheduledThreadPoolExecutor
继承自ThreadPoolExecutor
,内部使用DelayedWorkQueue
处理定时任务。
线程池类型 | 创建方式 | 核心问题 | 推荐指数 |
---|---|---|---|
固定大小线程池 | Executors.newFixedThreadPool(n) | 无界队列导致 OOM | ❌ 不推荐 |
缓存线程池 | Executors.newCachedThreadPool() | 线程数无限制导致系统崩溃 | ❌ 不推荐 |
单线程池 | Executors.newSingleThreadExecutor() | 无界队列导致 OOM | ❌ 不推荐 |
定时线程池 | Executors.newScheduledThreadPool( corePoolSize) | 需注意最大线程数和队列容量 | ✅ 谨慎使用 |
2、Executors 封装的优缺点
优点
- 简单易用:无需手动设置
ThreadPoolExecutor
的复杂参数,一行代码创建线程池。 - 标准化场景:针对常见场景(固定线程数、单线程、定时任务)提供预定义配置。
缺点
- 内存风险:
newFixedThreadPool
和newSingleThreadExecutor
使用无界队列LinkedBlockingQueue
,高负载下可能导致 OOM(Out Of Memory)。newCachedThreadPool
的最大线程数为Integer.MAX_VALUE
,可能创建过多线程耗尽系统资源。
- 缺乏灵活性:
无法自定义线程工厂、拒绝策略等关键参数,难以应对复杂业务场景。 - 性能隐患:
无界队列和默认拒绝策略(AbortPolicy
)可能导致任务积压或异常抛出,影响系统稳定性。
2. 参数调优建议
1. 根据任务类型设置线程数
- CPU 密集型:
corePoolSize = CPU核心数 + 1
(充分利用 CPU 资源)。 - IO 密集型:
corePoolSize = 2 * CPU核心数
(允许更多线程等待 IO)。
1、CPU 密集型(CPU-bound)
- 特征:任务的大部分时间用于CPU 计算(如复杂算法、数学运算、加密解密等),CPU 利用率高,而 I/O(磁盘 / 网络读写)操作较少或耗时较短。
- 瓶颈:CPU 计算能力成为性能瓶颈,任务执行时间主要受限于 CPU 的处理速度。
2、IO 密集型(IO-bound)
- 特征:任务的大部分时间用于等待 I/O 操作完成(如磁盘读写、网络请求、数据库查询等),CPU 利用率较低,I/O 操作的延迟成为性能瓶颈。
- 瓶颈:I/O 设备的吞吐量或延迟(如磁盘转速、网络带宽)限制了任务执行效率。
维度 | CPU 密集型 | IO 密集型 |
---|---|---|
主要耗时点 | CPU 计算 | I/O 操作(等待磁盘 / 网络) |
CPU 利用率 | 高(接近 100%) | 低(通常低于 50%) |
理想线程数 | CPU 核心数 + 1 | 更高(如 CPU 核心数 × 2 或更多) |
优化重点 | 算法效率、多核并行 | 减少 I/O 次数、异步化、缓存 |
典型工具 | 并行计算框架(如 Fork/Join) | 异步框架(如 Netty、OKHttp) |
2. 使用有界队列:优先选择 ArrayBlockingQueue
或 LinkedBlockingQueue(capacity)
,避免无界队列的内存风险。
3. 自定义拒绝策略:根据业务需求选择 CallerRunsPolicy
(调用者处理)或 DiscardOldestPolicy
(丢弃旧任务),而非默认的 AbortPolicy
。
3. 简单线程池的实现(固定线程数目)
- 定义任务队列:使用有界阻塞队列存储待执行任务。
- 创建工作线程:通过构造函数初始化固定数量的线程,每个线程循环从队列中取任务执行。
- 提交任务:将任务放入队列,队列满时阻塞等待。
- 执行任务:工作线程取出任务并执行。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class MyThreadPool{// 任务队列:使用有界队列存储待执行的任务,容量为1000private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);// 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) {throw new RuntimeException(e);}}});t.start();}}//将任务提交到线程池public void submit(Runnable runnable) throws InterruptedException {//队列满,阻塞queue.put(runnable);//添加任务}
}
public class Test {public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool = new MyThreadPool(4);for(int i=0;i<1000;i++){int n = i;myThreadPool.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务"+n+",当前线程为:"+Thread.currentThread().getName());}});}}
}