文章目录
- Ⅰ. 线程同步
- 一、`wait` && `notify`
- 二、`wait` 与 `sleep` 的区别
- Ⅱ. 单例模式
- 一、饿汉模式
- 二、懒汉模式
- Ⅲ. 阻塞队列
- 一、标准库中的阻塞队列 -- `BlockingQueue`
- 二、自主实现阻塞队列(理解原理、细节即可)
- Ⅳ. 线程池
- 一、Java 线程池总体架构
- 为什么常用 `ExecutorService` 而不用 `ThreadPoolExecutor` 直接接收线程池对象❓❓❓
- ① 面向接口编程的思想
- ② 常用的工厂方法都返回 `ExecutorService`
- ③ 实际开发只用到 `ExecutorService` 的方法就够了
- 二、快速上手
- ① 使用 `Executors` 创建线程池
- ② 提交任务到线程池
- ③ 关闭线程池
- 三、`ThreadPoolExecutor` 参数的理解💥💥💥
- 四、自主实现简易线程池(理解原理、细节即可)
- Ⅴ. 定时器
- 一、标准库中的定时器 -- `Timer`
- 二、自主实现定时器(理解原理、细节即可)
Ⅰ. 线程同步
一、wait&¬ify
下面的方法,都是Object类实现的,所以所有类都存在这些线程同步方法!
| 方法 | 描述 | 说明 |
|---|---|---|
void wait() | 无限期等待,直到被唤醒 | 必须在synchronized中使用 |
void wait(long timeout) | 最多等待timeout毫秒 | 超时未被唤醒也会继续执行 |
void notify() | 无参数,随机唤醒一个等待线程 | 只能唤醒一个,不能控制具体唤醒哪个线程 |
void notifyAll() | 无参数,唤醒所有等待该对象锁的线程 | 所有被唤醒线程会竞争锁,推荐用于并发协作 |
wait做的事情如下所示:
- 使当前执行代码的线程进行等待,腾出
CPU使用权,释放当前的锁 - 满足一定条件时被唤醒,重新尝试获取这个锁(不一定能拿到锁)
一般wait()的使用如下所示:
synchronized(locker){// 使用 while 防止 “虚假唤醒”while(!条件成立){locker.wait();// 🟢 这段逻辑是实现同步的关键一步,应该放在逻辑开头}// 当条件满足,才继续做事}✅注意事项:
wait()和notify()必须在synchronized内使用,否则会抛出IllegalMonitorStateException- 一般放在条件不满足的位置(比如队列满或空)
- 用
while而非if来包裹wait(),防止虚假唤醒
二、wait与sleep的区别
| sleep() | wait() | |
|---|---|---|
| 所属类 | Thread类的静态方法 | Object类的方法 |
是否需要锁(synchronized) | ❌ | ✅ 需要,且必须在同步代码块中使用 |
| 是否释放锁 | ❌ | ✅ 释放锁,等待期间不占有对象锁 |
是否释放CPU | ✅ | ✅ |
| 唤醒方式 | 超时唤醒 | 被notify()/notifyAll()或超时唤醒 |
| 是否可中断 | ✅ 抛出InterruptedException | ✅ 抛出InterruptedException |
| 是否用于线程间通信 | ❌ | ✅ 能,用于线程间协调与同步 |
| 主要用途 | 暂停线程(模拟延迟、限流等) | 条件不满足时挂起线程,等待其他线程通知继续执行 |
Ⅱ. 单例模式
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编写真正工程化。设计模式就是软件工程的基石脉络,如同大厦的结构一样。
一个类只能创建一个对象,这就是单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
一、饿汉模式
饿汉模式属于 “急切加载” 的方式,即在类加载时就立即创建单例对象,而不管是否会被使用。
- 优点:
- 实现简单,代码简洁。
- 线程安全,不需要额外的同步机制。
- 缺点:
- 若单例对象的成员数据过多,那么会导致整个程序启动变慢。
- 如果有多个单例类是相互依赖并且有初始化依赖顺序的,那么饿汉模式在创建的时候是控制不了这种依赖顺序。
publicclassStarvingSingleton{// 静态变量,存储单例对象,使用final修饰privatestaticfinalStarvingSingletoninstance=newStarvingSingleton();// 私有化构造方法,防止外部通过new创建对象privateStarvingSingleton(){}// 提供获取单例成员接口publicstaticStarvingSingletongetInstance(){returninstance;}}💥注意事项:
- 将
instance设为final,是为了防止其它代码意外修改单例对象的引用,从而避免创建多个实例的情况。
二、懒汉模式
懒汉模式的核心思想是 “延迟加载”,即只有在第一次使用单例对象时才创建它。这种方式可以节省资源,因为单例对象只有在真正需要时才会被创建!
- 优点:
- 因为对象在主程序之后才会创建,所以程序启动比饿汉模式要快。
- 只有在需要时才创建单例对象,避免了不必要的资源占用。
- 可以控制不同的单例类的依赖关系以及控制依赖顺序。
- 缺点:
- 涉及到多线程安全问题,需要加锁,实现更复杂。
publicclassLazySingleton{// 静态变量,存储单例对象,同时使用volatile修饰privatestaticvolatileLazySingletoninstance=null;// 私有化构造方法,防止外部通过new创建对象privateLazySingleton(){}// 提供一个公共的静态方法,返回单例对象publicstaticLazySingletongetInstance(){if(instance==null){// 第一次检查synchronized(LazySingleton.class){// 加锁if(instance==null){// 第二次检查instance=newLazySingleton();}}}returninstance;}}💥注意事项:
- 将
instance设为volatile,是为了防止编译器优化出现的指令重排序问题,导致上述第14行代码拿到的对象是null(重排序导致先赋值,再初始化,结果拿到的是没初始化前的null对象),此时一使用该对象,直接就奔溃了! - 这个双重判断,叫做 “双重检查锁定DCL”,第一层检查是为了避免不必要的加锁带来的效率问题,第二层检查是为了实现单例对象,目的不同!
Ⅲ. 阻塞队列
阻塞队列是一种特殊的队列,也遵守 “先进先出” 的原则,并且阻塞队列也是一种线程安全的数据结构,并且具有以下特性:
- 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
- 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素,
阻塞队列的一个典型应用场景就是 "生产者消费者模型”,这是一种非常典型的开发模型,如下图所示:
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,而是直接扔给阻塞队列;消费者不找生产者要数据,而是直接从阻塞队列里取。
下面是生产者消费者模型的优缺点:
- 优点:
- 削峰填谷。减少出现生产者资料生产少了之后消费者也消费的少,或者消费者消费地快导致生产者来不及生产的情况。
- 降低耦合度。生产者线程只负责生产资料然后放到阻塞队列中,而不关心消费者的任务;消费者线程只需要从阻塞队列中获取资料进行消费,而不关心生产者的任务。
- 减少资源竞争,提升程序效率。可以理解为有了阻塞队列之后,生产者和消费者都只能依次排队操作队列,且队列会自动安排进出顺序,这不就既高效又有秩序了吗?
- 缺点:
- 系统更加复杂。
- 引入队列的层数太多,会增加网络传输的开销。
一、标准库中的阻塞队列 –BlockingQueue
在Java标准库中,java.util.concurrent包提供了多种阻塞队列的实现,这些阻塞队列都实现了BlockingQueue接口。
以下是BlockingQueue接口的主要方法:
| 方法 | 描述 |
|---|---|
void put(E e) | 将元素插入队列的尾部,如果队列已满,则阻塞等待,直到有空间可用 |
E take() | 从队列头部取出并移除元素,如果队列为空,则阻塞等待,直到有元素可用 |
E poll(long timeout, TimeUnit unit) | 从队列头部取出并移除元素,如果队列为空,则等待最多指定的时间,如果在指定时间内没有元素可用,则返回null |
E poll() | 从队列头部取出并移除元素,如果队列为空,则立即返回null |
E peek() | 查看队列头部的元素,但不移除它,如果队列为空,则返回null |
int remainingCapacity() | 返回队列剩余的容量,即还可以插入多少元素 |
boolean offer(E e, long timeout, TimeUnit unit) | 将元素插入队列的尾部,如果队列已满,则等待最多指定的时间,如果在指定时间内没有空间可用,则返回false |
boolean offer(E e) | 将元素插入队列的尾部,如果队列已满,则立即返回false |
int drainTo(Collection<? super E> c) | 将队列中的所有元素转移到指定的集合中,返回转移的元素数量 |
int drainTo(Collection<? super E> c, int maxElements) | 将队列中的最多maxElements个元素转移到指定的集合中,返回转移的元素数量 |
💥注意事项:
- 插入和弹出队列的操作,只有
put()和take()有阻塞等待的效果,其它方法比如offer()、add()、poll()等是不带阻塞功能的,所以一般只用put()和take()即可。 BlockingQueue是一个接口,创建对象时候需要用以下常见的阻塞队列实现类:
| 队列类型 | 底层结构 | 有界性 | 处理优先级 | 线程安全机制 | 适用场景 |
|---|---|---|---|---|---|
| ArrayBlockingQueue | 数组(大小固定) | 有界(必须指定) | FIFO | ReentrantLock | 严格控制任务数量、防止OOM,适合内存敏感系统(如任务缓冲) |
| LinkedBlockingQueue | 链表 | 有界或无界 | FIFO | 插入与移除锁分离(putLock/takeLock) | 任务量大但可控时可用无界; 希望高并发性能但需限制任务量时选用有界 |
| SynchronousQueue | 无(容量为0) | 无存储 | N/A | ReentrantLock | 适合高并发短任务、任务直接移交等场景 |
| PriorityBlockingQueue | 堆结构 | 无界 | 优先级 | ReentrantLock | 任务需按优先级处理,如支付系统处理高金额、实时任务优先执行 |
| DelayQueue | 堆结构 | 无界 | 延迟 + 优先级 | ReentrantLock | 定时任务调度,如订单超时取消、缓存过期处理 |
二、自主实现阻塞队列(理解原理、细节即可)
💥注意事项:
- 在判断队列为空或者队列为满的时候,要用
while判断,而不能用if判断。因为比如队列为满的时候被唤醒了,也不一定队列就不满,有可能在唤醒期间有其它线程又插入了数据,此时就会出现问题!
publicclassMyBlockingQueue{// 使用循环队列模拟阻塞队列privateString[]arr;privateinthead=0;privateinttail=0;privateintsize;// 有效元素个数privatestaticfinalObjectlock=newObject();publicMyBlockingQueue(intcapacity){arr=newString[capacity];}publicvoidput(Stringvalue)throwsInterruptedException{synchronized(lock){// 用 while 来判断队列是否满,看看是否要阻塞while(size>=arr.length){lock.wait();}// 插入数据arr[tail]=value;tail=(tail+1)%arr.length;size++;// 唤醒消费者lock.notify();}}publicStringtake()throwsInterruptedException{synchronized(lock){// 用 while 来判断队列是否空,看看是否要阻塞while(size==0){lock.wait();}// 拿到数据Stringvalue=arr[head];head=(head+1)%arr.length;size--;// 唤醒生产者lock.notify();returnvalue;}}}Ⅳ. 线程池
一、Java 线程池总体架构
Java线程池的核心接口是Executor和ExecutorService,最常用的实现类是ThreadPoolExecutor。
并且Java还帮我们搞了一个工厂类Executors,用于创建特定需求的ThreadPoolExecutor,省去了麻烦的构造传参过程。
它们的关系如下图所示:
为什么常用
ExecutorService而不用ThreadPoolExecutor直接接收线程池对象❓❓❓① 面向接口编程的思想
- 灵活与扩展性:
ExecutorService是接口,ThreadPoolExecutor是实现类。接口能屏蔽实现细节,提高代码的灵活性。比如以后你想换成另一个线程池实现,比如ScheduledThreadPoolExecutor,只用改一行初始化、不用大改其他代码。- 更好的解耦:依赖接口而不是实现类,可以让你的代码更松耦合。测试、替换实现、未来新实现(比如自定义线程池)都更容易。
② 常用的工厂方法都返回
ExecutorService常见的线程池创建方法返回的本来就是
ExecutorService接口:
这样别人用你的代码/方法,只要依赖
ExecutorService的API就行,不用知道是不是ThreadPoolExecutor,还是别的啥池子。③ 实际开发只用到
ExecutorService的方法就够了
- 线程池的绝大多数常用操作,比如
execute()、submit()、shutdown()等等,全部定义在ExecutorService接口里。日常99%的场景都不需要依赖实现类的特有方法。- 只有极少数高级场景(比如你要去读线程池的活动线程数
activeCount,或者定制特殊的配置参数),才可能需要用到实现类ThreadPoolExecutor的专有方法。
二、快速上手
① 使用Executors创建线程池
publicstaticvoidmain(String[]args){// 1. 固定大小线程池:适用于负载稳定的场景ExecutorServicep1=Executors.newFixedThreadPool(10);// 2. 可缓存线程池:有任务就创建新线程,空闲线程会回收,适用于大量短生命周期的任务ExecutorServicep2=Executors.newCachedThreadPool();// 3. 单线程的线程池,保证任务顺序执行ExecutorServicep3=Executors.newSingleThreadExecutor();// 4. 定时/周期性线程池:适用于延迟或周期性任务调度ExecutorServicep5=Executors.newScheduledThreadPool(10);}线程池与阻塞队列的匹配关系如下表所示:
| 线程池类型 | 使用的阻塞队列 | 设计目标 |
|---|---|---|
| FixedThreadPool | LinkedBlockingQueue | 固定线程数,任务队列无界,避免线程频繁创建 |
| CachedThreadPool | SynchronousQueue | 线程数动态扩展,任务直接传递,适合短时任务 |
| SingleThreadExecutor | LinkedBlockingQueue | 单线程顺序执行任务 |
| ScheduledThreadPool | DelayedWorkQueue | 延迟或周期性任务调度 |
要手动创建线程池的话,可以看后面ThreadPoolExecutor参数的解释!
② 提交任务到线程池
创建出线程池之后,就要提交 “任务” 到线程池中,由线程池自己调度或创建新线程来完成该 “任务”。下面是Executor中给出的两个提交任务的接口:
| execute | submit(推荐⭐⭐⭐) | |
|---|---|---|
| 接收参数类型 | Runnable(无返回值) | Runnable |
| 返回值 | void(无返回) | Future(可获得返回和异常) |
| 任务异常处理 | 线程池会捕获但不会报告异常 | Future.get()时抛出ExecutionException |
| 任务中断支持 | 无 | 可以cancel中断任务 |
| 用途 | 简单任务,不关心结果 | 需要关心任务结果或异常 |
现代最佳实践其实推荐多用submit,因为哪怕暂时不需要返回值,有Future也能后续扩展,比如取消、结果统计等。
// 1. 创建一个指定数量线程的线程池ExecutorServicep1=Executors.newFixedThreadPool(10);// 2. 让线程池中10个线程来处理100个打印任务for(inti=0;i<100;++i){intindex=i;p1.submit(()->{// submit不处理返回值是没问题的!System.out.println(Thread.currentThread().getName()+":"+index);});}③ 关闭线程池
用shutdown()优雅关闭,不再接收新任务,等待现有任务执行完毕。
p1.shutdown();三、ThreadPoolExecutor参数的理解💥💥💥
ThreadPoolExecutorpool=newThreadPoolExecutor(intcorePoolSize,// 核心线程数intmaximumPoolSize,// 最大线程数longkeepAliveTime,// 非核心闲置线程最大存活时间TimeUnitunit,// keepAliveTime的时间单位BlockingQueue<Runnable>workQueue,// 任务队列ThreadFactorythreadFactory,// 线程工厂RejectedExecutionHandlerhandler// 拒绝策略);corePoolSize:核心线程数量(不会被回收,相当于正式员工,一旦录用,永不辞退)maximumPoolSize:最大线程数量(相当于正式员工 + 临时工的数量,当临时工那部分闲置超过了keepAliveTime,就会被炒掉)- 当最大活动线程数量超过
maximumPoolSize,新任务就会触发下面的 “拒绝策略”。
- 当最大活动线程数量超过
keepAliveTime:非核心闲置线程最大存活时间(即临时工允许的闲置时间)- 如果设置了
allowCoreThreadTimeOut(true),那么核心线程如果也空闲时间超过该值也会被回收。
- 如果设置了
unit:keepAliveTime的时间单位(纳秒、微妙、毫秒、秒、分钟、小时、天)workQueue:线程池内部存储待执行任务的阻塞队列threadFactory:创建线程的工厂类,控制创建线程的细节(如线程命名、优先级、是否为守护线程等)Java自带了现成的创建线程的工厂类,最常用的就是Executors.defaultThreadFactory()只有在有特殊需求时才需要自定义,比如:
- 想让线程名带有特定业务标识、更好排查问题
- 希望线程变成 “守护线程”
- 想统一捕获线程内部未捕获异常
- 对线程优先级有特殊要求
- 希望在线程创建时做监控或自定义初始化
handler:拒绝策略,就是任务太多,超过workQueue的容量后,要怎么处理,有四种内置选项:AbortPolicy(默认):直接抛出RejectedExecutionException异常DiscardPolicy:直接丢弃DiscardOldestPolicy:丢弃队列里最老的任务,然后再尝试入队当前任务CallerRunsPolicy:由提交任务的线程自己执行该任务
ThreadPoolExecutorpool=newThreadPoolExecutor(5,// corePoolSize10,// maximumPoolSize60,// keepAliveTimeTimeUnit.SECONDS,// 单位newArrayBlockingQueue<>(100),// 有界队列Executors.defaultThreadFactory(),// 默认线程工厂newThreadPoolExecutor.AbortPolicy()// 拒绝策略);// 如果要手动自定义线程工厂,可以按照下面这样子处理,然后传myFactory给ThreadPoolExecutor即可AtomicIntegerthreadId=newAtomicInteger(1);ThreadFactorymyFactory=r->{Threadt=newThread(r,"mythread-"+threadId.getAndIncrement());t.setDaemon(true);// 设置线程为守护线程returnt;};💥注意事项:
- 参数中
BlockingQueue<Runnable>中的Runnable和ThreadFactory.newThread(Runnable r)中的Runnable是不同的,区别如下所示:ThreadFactory.newThread(Runnable r)中的r- 这个
Runnable r不是你的 “具体业务”,而是线程池 “工作线程” 的调度骨架(Worker)。它的职责就是反复从任务队列BlockingQueue<Runnable>中取任务出来执行,一旦取到任务就执行任务的run()。 - 换句话说,这个
Runnable r更像是 “消费者行为的实现”,并不是你自己提交的具体业务任务。
- 这个
- 阻塞队列或者
submit(Runnable r)中的r- 这才是你写的 “具体业务”:比如下载文件、处理订单、统计数据……
- 需要注意的是
submit(Runnable r)中的r和阻塞队列BlockingQueue<Runnable>中的Runnable是一回事,都是指具体业务,阻塞队列相当于提供了一个存放submit提交业务的空间! - 它被丢进线程池的任务队列,等消费者(即线程池
Worker线程)来拿走处理。
- 总结:
submit(Runnable r)中的r是 “具体业务”,这些业务会被当作 “原料” 投向线程池由工作线程拿去处理,而工作线程的属性等是由ThreadFactory.newThread(Runnable r)中的r提供的,并且这些工作线程由线程池自主调度,不需要程序员手动处理。
四、自主实现简易线程池(理解原理、细节即可)
实现一个简易的线程池,是为了帮助理解线程池中阻塞队列的作用、线程的执行这些环节,实现起来并不难,只需要用一个阻塞队列,然后启动线程执行阻塞队列中的任务即可!
publicclassMyThreadPool{privateBlockingQueue<Runnable>qe=newLinkedBlockingQueue<>();publicMyThreadPool(intn){for(inti=0;i<n;++i){Threadthread=newThread(()->{// 让每个线程循环处理阻塞队列中的业务,然后执行while(true){try{Runnabler=qe.take();r.run();}catch(InterruptedExceptione){thrownewRuntimeException(e);}}});thread.start();// 别忘了启动线程}}// 将具体业务插入到阻塞队列中,具体调度由阻塞队列自己处理publicvoidsubmit(Runnabler){try{qe.put(r);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}测试代码非常简单,提交任务让线程池去处理即可,如下所示:
publicstaticvoidmain(String[]args)throwsInterruptedException{MyThreadPoolmtp=newMyThreadPool(5);for(inti=0;i<100;++i){intindex=i;mtp.submit(()->{System.out.println(Thread.currentThread().getName()+"处理"+index+"号业务");});}}Ⅴ. 定时器
定时器是软件开发中的一个重要组件,就是程序中闹钟,时间到了就会执行某段提前设定好的代码。比如网络通信中的最大未响应时间,超过一段时间没有收到数据,此时程序应该尝试发起重新连接等等情况。
一、标准库中的定时器 –Timer
Timer是Java中提供的一种简单的定时任务调度工具类,用于安排一个任务在指定的时间执行一次,或者周期性地执行。
Timer的接口如下表所示:
| 方法签名 | 说明 | 是否周期执行 | 延迟类型 |
|---|---|---|---|
schedule(TimerTask task, long delay) | 延迟 delay 毫秒后执行一次任务 | ❌ | 固定延迟 |
schedule(TimerTask task, Date time) | 在指定时间点 time 执行一次任务 | ❌ | 固定时间点 |
schedule(TimerTask task, long delay, long period) | 延迟 delay 毫秒后开始,每隔 period 毫秒执行一次任务 | ✅ | 固定延迟(上次任务结束后再延迟) |
schedule(TimerTask task, Date firstTime, long period) | 指定首次时间 firstTime,之后每隔 period 毫秒执行 | ✅ | 固定延迟 |
scheduleAtFixedRate(TimerTask task, long delay, long period) | 延迟 delay 毫秒后开始,每隔 period 毫秒强制执行一次(不管上次是否完成) | ✅ | 固定频率(时间点为准) |
scheduleAtFixedRate(TimerTask task, Date firstTime, long period) | 指定首次时间点 firstTime,之后按固定频率周期执行 | ✅ | 固定频率 |
cancel() | 取消当前定时器中所有已安排的任务 | — | — |
purge() | 清除已被取消的任务(返回清除的个数) | — | — |
💥注意事项:
- 固定延迟:下一个任务从上一个任务执行完毕后开始计算延迟,不会并发,适合非精确定时。
- 固定频率:下一个任务的开始时间是按理想周期时间表推进,即使上一个没执行完也尝试补上,可能并发,适合高精度周期调度。
在使用的时候,Timer需要配合TimerTask一起使用,如下所示:
// 1. 定义Timer对象Timertimer=newTimer();// 2. 调用schedule设置定时任务TimerTask,以及定时时长,其中TimerTask要重写run()timer.schedule(newTimerTask(){@Overridepublicvoidrun(){System.out.println("定时器执行3000ms");}},3000);其中TimerTask实现了Runnable接口,本质上就是在Runnable的基础上增加了一些属性、方法等,所以定义TimerTask时候重写一下其中的run()即可!
二、自主实现定时器(理解原理、细节即可)
// 任务类,保存任务以及任务执行时间classTaskimplementsComparable<Task>{privateRunnabletask;privatelongtime;// 为了方便判断时间是否到达, 所以保存绝对的时间戳publicTask(Runnabletask,longtime){this.task=task;this.time=System.currentTimeMillis()+time;// 注意这里存放的是时间戳,所以要计算一下}publicRunnablegetTask(){returntask;}publiclonggetTime(){returntime;}@OverridepublicintcompareTo(Tasko){return(int)(this.time-o.time);}}publicclassMyTimer{// 使用优先级队列作为存放定时任务的容器,且要以时间间隔最小的任务作为堆顶privatePriorityQueue<Task>qe=newPriorityQueue<>();// 队列涉及到多线程操作,需要进行加锁// 并且为了避免“忙等”,可以用wait()和notify()配合,来让出CPU资源privatefinalObjectlocker=newObject();publicMyTimer(){// 构建线程去处理定时任务Threadthread=newThread(()->{while(true){// 加锁try{synchronized(locker){// 判断是否有定时任务if(qe.isEmpty()==true){// 1. 直接continue会导致忙等,浪费cpu资源locker.wait();}// 走到这说明存在定时任务,则判断是否到达执行时间Taskt=qe.peek();if(t.getTime()>System.currentTimeMillis()){// 2. 时间还没到,直接continue同样会造成忙等,浪费cpu资源,所以这里同样使用wait()等待唤醒// 不同的是这里要设置超时时间,因为在wait()阻塞到该定时任务时间到之前如果没有新的任务来的话,这个线程// 都不会被唤醒,导致任务没有及时处理,所以要设置超时时间为剩余等待时间!longgap=t.getTime()-System.currentTimeMillis();locker.wait(gap);}else{// 时间到了,执行任务t.getTask().run();qe.poll();}}}catch(InterruptedExceptione){thrownewRuntimeException(e);}}});thread.start();}// 插入定时任务publicvoidschedule(Runnabler,longdelay){synchronized(locker){qe.offer(newTask(r,delay));locker.notify();}}}💥注意事项:
- 标准库里面有一个现成的阻塞优先级队列
BlockingPriorityQueue,但这里不用它的原因是因为BlockingPriorityQueue没有定时阻塞的功能,在构造方法中判断是否到达执行任务的时间,没办法进行定时阻塞,从而没办法实现该逻辑,所以只能用PriorityQueue加上synchronized来实现! - 一旦程序中涉及到加锁的地方存在条件判断的时候,要考虑清楚当条件不符合时候会不会出现形如 “忙等” 的情况,然后分析这种 “忙等” 的情况对程序带来的效率问题大不大,大的话最好要用
wait()和notify()配合组合和唤醒,避免CPU资源浪费!