Java EE初阶——定时器和线程池

发布于:2025-05-21 ⋅ 阅读:(15) ⋅ 点赞:(0)

 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开始计算)。

  • 第二个参数是延迟时间,单位为毫秒。定时器内部有一个线程,到延迟时间后就会执行任务。

执行流程:

  1. 程序启动,创建Timer实例

  2. 调用schedule方法安排任务

  3. Timer内部的后台线程开始计时

  4. 2秒后,Timer的后台线程调用TimerTaskrun方法

注意事项:

  1. Timer创建的线程默认是非守护线程(用户线程),即使main方法结束,程序也不会退出,除非调用timer.cancel()或所有非守护线程都结束。

  2. 如果需要在任务执行后让程序退出,可以:

    • 调用timer.cancel()取消定时器

    • 或者将定时器设置为守护线程:Timer timer = new Timer(true);

  3. 如果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() 不会中断它,但后续任务不会再被执行。

注意事项:

  1. cancel() 只能调用一次

    • 如果多次调用 cancel(),后续调用不会有任何效果。

    • 调用后,Timer 对象不能再安排新任务(会抛出 IllegalStateException)。

  2. Timer 线程不会立即终止

    • cancel() 只是标记 Timer 为已取消,正在执行的任务(如果有)会继续完成。

    • 如果希望立即停止正在运行的任务,可以使用 Thread.interrupt() 机制(但 TimerTask 本身不支持中断)。

  3. 具有不可逆性

              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() 不会立即生效,线程会继续阻塞,直到阻塞解除后才会检测到中断状态,此时任务可能继续执行提前终止(取决于代码是否处理中断)。
  1. 替代方案(推荐)

    • 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. 优势

  1. 降低资源消耗:减少线程创建 / 销毁的开销(每个线程创建约需消耗 1MB 栈内存)。
  2. 提高响应速度:任务到达时无需等待线程创建,直接由空闲线程处理。
  3. 控制并发数量:通过设置线程池大小,避免因线程过多导致的内存溢出或 CPU 过度切换。
  4. 统一管理:提供线程监控、异常处理等机制,便于系统调优
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. 工厂模式

定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。

工厂模式的核心角色

  1. 抽象产品(Product)
    定义所有具体产品的公共接口或抽象类,约束产品的行为。
  2. 具体产品(Concrete Product)
    实现抽象产品接口,是实际创建的对象。
  3. 抽象工厂(Factory)
    定义创建产品的公共接口(如createProduct()方法)。
  4. 具体工厂(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 封装的优缺点

优点
  1. 简单易用:无需手动设置 ThreadPoolExecutor 的复杂参数,一行代码创建线程池。
  2. 标准化场景:针对常见场景(固定线程数、单线程、定时任务)提供预定义配置。
缺点
  1. 内存风险
    • newFixedThreadPool 和 newSingleThreadExecutor 使用无界队列 LinkedBlockingQueue,高负载下可能导致 OOM(Out Of Memory)
    • newCachedThreadPool 的最大线程数为 Integer.MAX_VALUE,可能创建过多线程耗尽系统资源。
  2. 缺乏灵活性
    无法自定义线程工厂、拒绝策略等关键参数,难以应对复杂业务场景。
  3. 性能隐患
    无界队列和默认拒绝策略(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. 简单线程池的实现(固定线程数目)

  1. 定义任务队列:使用有界阻塞队列存储待执行任务。
  2. 创建工作线程:通过构造函数初始化固定数量的线程,每个线程循环从队列中取任务执行。
  3. 提交任务:将任务放入队列,队列满时阻塞等待。
  4. 执行任务:工作线程取出任务并执行。
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());
                }
            });
        }
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到