Java EE(8)——线程安全总结(翻新版)——定时器(Timer)&线程池(ThreadPoolExecutor)

发布于:2025-04-16 ⋅ 阅读:(18) ⋅ 点赞:(0)

1.Timer

1.1Timer基本介绍

1.Timer的主要作用

任务调度:Timer允许你安排一个任务在未来的某个时间点执行,或者以固定的间隔重复执行
后台执行:Timer可以使用一个后台线程来执行任务,这意味着调度和执行任务不会阻塞主线程(主线程结束后后台线程跟着结束)
简单易用:Timer提供了一个相对简单的方式来处理定时任务,适合用于不需要复杂调度的场景

2.Timer的构造方法

//1.默认构造方法
//创建一个Timer对象,是一个后台线程,并使用线程的默认名字
public Timer() {
        this("Timer-" + serialNumber());
}
//2.指定线程名字的构造方法
//创建一个Timer对象,是一个后台线程,并使用指定的线程名字
public Timer(String name) {
        thread.setName(name);
        thread.start();
}
//3.指定是否为后台线程的构造方法
//传入true,是后台线程;传入false,是前台线程
public Timer(boolean isDaemon) {
        this("Timer-" + serialNumber(), isDaemon);
}
//4.指定线程名字和是否为后台线程的构造方法
public Timer(String name, boolean isDaemon) {
        thread.setName(name);
        thread.setDaemon(isDaemon);
        thread.start();
}

3.Timer中的schedule方法

(1)schedule(TimerTask task, Date time):安排任务在指定的时间执行一次

public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("延迟三秒执行");
            }
        };
        //使用Date对象来指定具体的执行时间
        //new Date(System.currentTimeMillis()+1000表示当前时间等待1000ms
        timer.schedule(timerTask,new Date(System.currentTimeMillis()+1000));
}

(2)schedule(TimerTask task, Date firstTime, long period):安排任务在指定的时间首次执行,然后每隔一段时间重复执行

public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("延迟三秒执行");
            }
        };
        //当前时间等待1000ms后第一次执行任务
        //此后每间隔1000ms就执行一次任务
        timer.schedule(timerTask,new Date(System.currentTimeMillis()+1000),1000);
}

(3)schedule(TimerTask task, long delay):安排任务在指定的延迟时间后执行一次(相对于当前时间)

public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("延迟三秒执行");
            }
        };
        //当前时间延迟3000ms后执行
        timer.schedule(timerTask,3000);
}

(4)schedule(TimerTask task, long delay, long period):安排任务在指定的延迟时间后首次执行,然后每隔一段时间重复执行

public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("延迟三秒执行");
            }
        };
        //当前时间延迟3000ms后执行
        //此后每间隔3000ms就执行一次任务
        timer.schedule(timerTask,30003000);
}

问题:上述构造方法中,使用Date对象指定时间和使用long类型数据指定时间有什么区别?

Date对象可以指定一个具体的时间,比如在202511120101秒执行任务;
而传入一个long类型数据表示以当前时间为基准,延迟指定时间后执行任务

1.2模拟实现MyTimer

1.2.1大体框架

MyTimer类:
(1)Thread线程:用来执行任务
(2)PriorityQueue(小根堆):存放任务,并且可以按照时间先后顺序取出任务
(3)MyTimer构造方法:启动thread
(4)schedule:实例化/添加任务
任务
MyTask类
(1)time:保存任务要执行的时间
(2)Runnable:重写run方法
(3)run方法:执行run方法
(4)getTime:获取人物的执行时间
(5)compareTo方法:保证放进小根堆的任务必须是可比较的

1.2.2MyTask类

在这里插入图片描述

1.2.3MyTime类

在这里插入图片描述

1.2.4代码演示

在这里插入图片描述

1.3MyTimer源码

import java.util.PriorityQueue;
/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 38917
 * Date: 2025-03-05
 * Time: 19:26
 */
//描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
    private final long time;
    private final Runnable runnable;

    public MyTimerTask(Runnable runnable,long delay){
        this.runnable = runnable;
        this.time = (System.currentTimeMillis() + delay);
    }
    //执行run中的代码块
    public void run(){
        runnable.run();
    }
    //比较
    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.time - o.time);
    }
    //获取时间
    public long getTime() {
        return time;
    }
}

class MyTimer {
    //创建线程
    Thread thread;
    //存放任务
    private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    //创建锁对象
    private final Object locker = new Object();
    //实例化任务
    public void schedule(Runnable runnable,long delay){
        synchronized (locker) {
            //实例化任务
            MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
            //添加任务
              queue.offer(myTimerTask);
            locker.notify();
        }
    }
    //扫描任务队列,执行任务的线程
    public MyTimer(){
        thread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                //锁是保护队列
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            locker.wait();
                        }
                        MyTimerTask myTimerTask = queue.peek();
                        long curTime = System.currentTimeMillis();
                        if (curTime >= myTimerTask.getTime()) {
                            queue.poll();
                            //执行任务
                            myTimerTask.run();
                        } else {
                            locker.wait(myTimerTask.getTime() - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(() -> System.out.println("400"),400);
        myTimer.schedule(() -> System.out.println("300"),300);
        myTimer.schedule(() -> System.out.println("200"),200);
        myTimer.schedule(() -> System.out.println("100"),100);

        Thread.sleep(500);
        System.out.println("Hello Main");
    }
}

2.线程池

2.1什么是线程池?

1.概念:线程池是一种管理和复用线程的编程模式。它预先创建一定数量的线程,在执行任务需要时,将任务分配给这些线程,从而提高运行效率

2.主要特点
线程复用:当线程执行完一个任务时,不会立即销毁,而是等待下一个任务的到来(当然这种等待是有时间限制的),这样避免了频繁的创建和销毁线程
动态调整:根据实际环境需要动态调整线程数量,以达到最佳性能
任务队列:线程池会维护一个任务队列,用于存放待执行的任务,当线程空闲时,从队列中取出任务并执行

2.2 标准库线程池参数介绍

在这里插入图片描述
1.int corePoolSize:核心线程数
2.int maximumPoolSize:最大线程数
3.long keepAliveTime:非核心线程的空闲时的最大存活时间。假如corePoolSize=4,maximumPoolSize=8,有四个非核心线程,这类线程空闲时,不能一直等待新任务的到来,当等待时间超过keepAliveTime后就会销毁
4.TimeUnit unit:long keepAliveTime的时间单位
5.BlockingQueue workQueue:任务队列
6.ThreadFactory threadFactory:线程工厂,用于创建新线程的工厂
7.RejectedExecutionHandler handler拒绝策略

2.3线程池的执行流程

假设现在有一个线程池:
核心线程数2,最大线程数4,等待队列2
(1)任务数量<=2(A,B)时,由核心线程执行任务
(2) 2<任务数量<=4(A,B,C,D)时,核心线程无法同时处理所有任务,未被执行的任务(C,D)将会进入等待队列中等待核心线程执行
(3)4<任务数量<=6(A,B,C,D,E,F),此时等待队列也满了,线程池就会就会开放非核心线程来执行任务,C和D任务继续在等待队列中等待,新添加的E和F任务由非核心线程来执行
(4)任务数量>6,核心线程,等待队列,非核心线程都被任务所占用,仍然无法满足需求,此时就会触发线程池的拒绝策略

RejectedExecutionHandler handler四大拒绝策略

在这里插入图片描述
1.AbortPolicy:直接抛异常
在这里插入图片描述
2.CallerRunsPolicy:由提交该任务的线程来执行在这里插入图片描述
3.DiscardPolicy:丢弃新任务
在这里插入图片描述
4.DiscardOldestPolicy:丢弃最老的任务
在这里插入图片描述

2.4模拟实现一个简单的线程池

在这里插入图片描述

3.线程安全问题总结

(1)使用没有共享资源的模型

(2)使用共享资源只读,不写的模型

  1.  不需要写共享资源的模型
    
  2.  使用不可变对象
    

(3)直面线程安全(重点)

  1.  保证原子性:synchronized
    
  2.  保证顺序性:volatile(取消指令重排序)
    
  3.  保证可见性:synchronized(上锁时将主内存中的数据同步到工作内存,解锁时将工作内存的数据同步到主内存),volatile(强制从主内存读取数据)
    

4.小结

基础的线程安全问题以及常见的四种设计模式(单例生产者&消费者定时器线程池)至此全部都介绍完毕了,下节开始就从锁策略开始讲解


网站公告

今日签到

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