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,3000,3000);
}
问题:上述构造方法中,使用Date对象指定时间和使用long类型数据指定时间有什么区别?
Date对象可以指定一个具体的时间,比如在2025年1月1日12时01分01秒执行任务;
而传入一个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)使用共享资源只读,不写的模型
不需要写共享资源的模型
使用不可变对象
(3)直面线程安全(重点)
保证原子性:synchronized
保证顺序性:volatile(取消指令重排序)
保证可见性:synchronized(上锁时将主内存中的数据同步到工作内存,解锁时将工作内存的数据同步到主内存),volatile(强制从主内存读取数据)
4.小结
基础的线程安全问题以及常见的四种设计模式(单例,生产者&消费者,定时器,线程池)至此全部都介绍完毕了,下节开始就从锁策略开始讲解