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 秒执行任务1
timer.schedule(new TimerTask(){//创建了一个TimerTask实例
//重新 run 方法,
@Override
public 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() {
@Override
public void run() {
System.out.println("任务1");
}
},2000);
timer.schedule(new Runnable() {
@Override
public 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() {
@Override
public 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{
// 任务队列:使用有界队列存储待执行的任务,容量为1000
private 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() {
@Override
public void run() {
System.out.println("执行任务"+n+",当前线程为:"+Thread.currentThread().getName());
}
});
}
}
}