news 2026/9/25 6:46:07

【JavaSE】十五、线程同步wait | notify 单例模式 阻塞队列 线程池 定时器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【JavaSE】十五、线程同步wait | notify 单例模式 阻塞队列 线程池 定时器

文章目录

  • Ⅰ. 线程同步
    • 一、`wait` && `notify`
    • 二、`wait` 与 `sleep` 的区别
  • Ⅱ. 单例模式
    • 一、饿汉模式
    • 二、懒汉模式
  • Ⅲ. 阻塞队列
    • 一、标准库中的阻塞队列 -- `BlockingQueue`
    • 二、自主实现阻塞队列(理解原理、细节即可)
  • Ⅳ. 线程池
    • 一、Java 线程池总体架构
      • 为什么常用 `ExecutorService` 而不用 `ThreadPoolExecutor` 直接接收线程池对象❓❓❓
        • ① 面向接口编程的思想
        • ② 常用的工厂方法都返回 `ExecutorService`
        • ③ 实际开发只用到 `ExecutorService` 的方法就够了
    • 二、快速上手
      • ① 使用 `Executors` 创建线程池
      • ② 提交任务到线程池
      • ③ 关闭线程池
    • 三、`ThreadPoolExecutor` 参数的理解💥💥💥
    • 四、自主实现简易线程池(理解原理、细节即可)
  • Ⅴ. 定时器
    • 一、标准库中的定时器 -- `Timer`
    • 二、自主实现定时器(理解原理、细节即可)

Ⅰ. 线程同步

一、wait&&notify

下面的方法,都是Object类实现的,所以所有类都存在这些线程同步方法!

方法描述说明
void wait()无限期等待,直到被唤醒必须在synchronized中使用
void wait(long timeout)最多等待timeout毫秒超时未被唤醒也会继续执行
void notify()无参数,随机唤醒一个等待线程只能唤醒一个,不能控制具体唤醒哪个线程
void notifyAll()无参数,唤醒所有等待该对象锁的线程所有被唤醒线程会竞争锁,推荐用于并发协作

wait做的事情如下所示:

  1. 使当前执行代码的线程进行等待,腾出CPU使用权,释放当前的锁
  2. 满足一定条件时被唤醒,重新尝试获取这个锁(不一定能拿到锁)

一般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;}}

💥注意事项:

  1. 将instance设为volatile,是为了防止编译器优化出现的指令重排序问题,导致上述第14行代码拿到的对象是null(重排序导致先赋值,再初始化,结果拿到的是没初始化前的null对象),此时一使用该对象,直接就奔溃了!
  2. 这个双重判断,叫做 “双重检查锁定DCL”,第一层检查是为了避免不必要的加锁带来的效率问题,第二层检查是为了实现单例对象,目的不同!

Ⅲ. 阻塞队列

阻塞队列是一种特殊的队列,也遵守 “先进先出” 的原则,并且阻塞队列也是一种线程安全的数据结构,并且具有以下特性:

  • 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
  • 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素,

阻塞队列的一个典型应用场景就是 "生产者消费者模型”,这是一种非常典型的开发模型,如下图所示:

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,而是直接扔给阻塞队列;消费者不找生产者要数据,而是直接从阻塞队列里取。

下面是生产者消费者模型的优缺点:

  1. 优点:
    1. 削峰填谷。减少出现生产者资料生产少了之后消费者也消费的少,或者消费者消费地快导致生产者来不及生产的情况。
    2. 降低耦合度。生产者线程只负责生产资料然后放到阻塞队列中,而不关心消费者的任务;消费者线程只需要从阻塞队列中获取资料进行消费,而不关心生产者的任务。
    3. 减少资源竞争,提升程序效率。可以理解为有了阻塞队列之后,生产者和消费者都只能依次排队操作队列,且队列会自动安排进出顺序,这不就既高效又有秩序了吗?
  2. 缺点:
    1. 系统更加复杂。
    2. 引入队列的层数太多,会增加网络传输的开销。

一、标准库中的阻塞队列 –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数组(大小固定)有界(必须指定)FIFOReentrantLock严格控制任务数量、防止OOM,适合内存敏感系统(如任务缓冲)
LinkedBlockingQueue链表有界或无界FIFO插入与移除锁分离(putLock/takeLock)任务量大但可控时可用无界; 希望高并发性能但需限制任务量时选用有界
SynchronousQueue无(容量为0)无存储N/AReentrantLock适合高并发短任务、任务直接移交等场景
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);}

线程池与阻塞队列的匹配关系如下表所示:

线程池类型使用的阻塞队列设计目标
FixedThreadPoolLinkedBlockingQueue固定线程数,任务队列无界,避免线程频繁创建
CachedThreadPoolSynchronousQueue线程数动态扩展,任务直接传递,适合短时任务
SingleThreadExecutorLinkedBlockingQueue单线程顺序执行任务
ScheduledThreadPoolDelayedWorkQueue延迟或周期性任务调度

要手动创建线程池的话,可以看后面ThreadPoolExecutor参数的解释!

② 提交任务到线程池

创建出线程池之后,就要提交 “任务” 到线程池中,由线程池自己调度或创建新线程来完成该 “任务”。下面是Executor中给出的两个提交任务的接口:

executesubmit(推荐⭐⭐⭐)
接收参数类型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
      1. 这个Runnable r不是你的 “具体业务”,而是线程池 “工作线程” 的调度骨架(Worker)。它的职责就是反复从任务队列BlockingQueue<Runnable>中取任务出来执行,一旦取到任务就执行任务的run()。
      2. 换句话说,这个Runnable r更像是 “消费者行为的实现”,并不是你自己提交的具体业务任务。
    • 阻塞队列或者submit(Runnable r)中的r
      1. 这才是你写的 “具体业务”:比如下载文件、处理订单、统计数据……
      2. 需要注意的是submit(Runnable r)中的r和阻塞队列BlockingQueue<Runnable>中的Runnable是一回事,都是指具体业务,阻塞队列相当于提供了一个存放submit提交业务的空间!
      3. 它被丢进线程池的任务队列,等消费者(即线程池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();}}}

💥注意事项:

  1. 标准库里面有一个现成的阻塞优先级队列BlockingPriorityQueue,但这里不用它的原因是因为BlockingPriorityQueue没有定时阻塞的功能,在构造方法中判断是否到达执行任务的时间,没办法进行定时阻塞,从而没办法实现该逻辑,所以只能用PriorityQueue加上synchronized来实现!
  2. 一旦程序中涉及到加锁的地方存在条件判断的时候,要考虑清楚当条件不符合时候会不会出现形如 “忙等” 的情况,然后分析这种 “忙等” 的情况对程序带来的效率问题大不大,大的话最好要用wait()和notify()配合组合和唤醒,避免CPU资源浪费!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 5:50:25

25、SSH应用案例解析:安全访问与数据传输方案

SSH应用案例解析:安全访问与数据传输方案 在网络安全领域,SSH(Secure Shell)协议凭借强大的加密和身份验证功能,为远程访问和数据传输提供了安全保障。本文将通过三个具体案例,深入探讨SSH在不同场景下的配置与应用。 案例一:多用途SSH服务器配置 本案例涉及两个VShe…

作者头像 李华
网站建设 2026/9/25 6:55:38

1、深入了解 SSH:功能、优势与实施指南

深入了解 SSH:功能、优势与实施指南 1. SSH 概述 Secure Shell(SSH)是一种用途广泛的实用工具,它可以被描述为协议、加密工具、客户端/服务器应用程序或命令接口。SSH 凭借其多样的服务以及安全提供这些服务的能力,成为许多企业网络中的重要组成部分。 1.1 SSH1 与 SSH…

作者头像 李华
网站建设 2026/9/25 6:56:46

17、文本处理与输入输出操作指南

文本处理与输入输出操作指南 1. 常用文本处理命令 在文本处理中,有几个常用的命令可以帮助我们完成各种任务。 1.1 tr 命令 tr 命令可以用于字符替换和文件加密。例如,在显示 MyFile 文件时,tr 可以将单词 “Halloween” 改为 “Haloween”。同时,它还能使用 ROT13 方法…

作者头像 李华
网站建设 2026/9/24 21:00:14

科研写作新范式:解锁书匠策AI期刊论文功能的隐藏生产力

在科研写作的赛道上&#xff0c;研究者往往需要与文献海洋博弈、与逻辑漏洞周旋、与格式规范较劲。而当人工智能技术深度渗透学术场景&#xff0c;一款名为书匠策AI的科研工具正以“问题导向的智能辅助”重构期刊论文创作生态。本文将以创新视角拆解其核心功能&#xff0c;揭示…

作者头像 李华
网站建设 2026/9/24 21:44:44

13、SSH在网络设备中的应用与安全管理

SSH在网络设备中的应用与安全管理 1. Cisco设备的SSH配置 1.1 Cisco交换机 Cisco Catalyst操作系统(CatOS)从6.1版本开始支持SSH,不过仅支持SSH版本1。尽管SSH 1存在安全隐患,但它替代Telnet后,极大增强了网络设备的安全性。支持SSH的Cisco交换机有Catalyst 3550、4000…

作者头像 李华