news 2026/3/30 17:22:15

【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(),防止虚假唤醒

二、waitsleep的区别

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线程池的核心接口是ExecutorExecutorService,最常用的实现类是ThreadPoolExecutor

并且Java还帮我们搞了一个工厂类Executors,用于创建特定需求的ThreadPoolExecutor,省去了麻烦的构造传参过程。

它们的关系如下图所示:

为什么常用ExecutorService而不用ThreadPoolExecutor直接接收线程池对象❓❓❓

① 面向接口编程的思想
  • 灵活与扩展性:ExecutorService是接口,ThreadPoolExecutor是实现类。接口能屏蔽实现细节,提高代码的灵活性。比如以后你想换成另一个线程池实现,比如ScheduledThreadPoolExecutor,只用改一行初始化、不用大改其他代码。
  • 更好的解耦:依赖接口而不是实现类,可以让你的代码更松耦合。测试、替换实现、未来新实现(比如自定义线程池)都更容易。
② 常用的工厂方法都返回ExecutorService

常见的线程池创建方法返回的本来就是ExecutorService接口:

这样别人用你的代码/方法,只要依赖ExecutorServiceAPI就行,不用知道是不是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),那么核心线程如果也空闲时间超过该值也会被回收。
  • unitkeepAliveTime的时间单位(纳秒、微妙、毫秒、秒、分钟、小时、天)
  • 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>中的RunnableThreadFactory.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

TimerJava中提供的一种简单的定时任务调度工具类,用于安排一个任务在指定的时间执行一次,或者周期性地执行。

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/3/27 4:17:16

16、Linux系统用户、组管理及文本流处理实用指南

Linux系统用户、组管理及文本流处理实用指南 1. 用户与组管理基础 在Linux系统中,用户和组管理是系统管理员的重要工作。所有用户账户信息存储在 /etc/passwd 文件中,每行定义一个用户,包含以下字段: - 用户名 - 密码 - 用户ID(UID) - 组ID(GID) - 注释 - 主…

作者头像 李华
网站建设 2026/3/30 17:31:38

26、SSH在跨系统文件访问中的应用与配置

SSH在跨系统文件访问中的应用与配置 1. 架构需求概述 从架构角度来看,关键需求相对较少。由于工程部门位于内部网络,而Windows文件服务器也处于该网络中,默认情况下每个客户端都能够访问该服务器。所有客户端需要使用OpenSSH SFTP客户端来访问SSH服务器上的SFTP子系统,或…

作者头像 李华
网站建设 2026/3/14 9:18:07

Harmony开发之设备发现与连接——分布式操作的起点

Harmony开发之设备发现与连接——分布式操作的起点 引入&#xff1a;自动发现附近可用设备 想象一下这样的场景&#xff1a;当你走进家门&#xff0c;手机自动发现并连接上家里的智能音响&#xff0c;开始播放你喜欢的音乐&#xff1b;当你在会议室做演示时&#xff0c;平板自动…

作者头像 李华
网站建设 2026/3/14 14:32:09

当科研不再“大海捞针”:用智能工具重构你的期刊论文探索路径

在当今学术生态中&#xff0c;期刊论文不仅是知识的载体&#xff0c;更是科研工作者的“第二语言”。然而&#xff0c;面对浩如烟海的文献海洋&#xff0c;如何精准定位关键信息、高效追踪领域前沿、系统梳理研究脉络&#xff0c;成了无数研究者日复一日的“隐性负担”。传统数…

作者头像 李华
网站建设 2026/3/30 9:15:20

23、安全Web环境中的代理技术

安全Web环境中的代理技术 1. 代理服务器与SSH概述 在任何网络环境中,代理服务器的使用都能简化终端用户的操作环境。代理服务器是一种代表其他实体发起请求的应用程序。如今,大多数使用的代理服务器是Web代理。当客户端机器尝试访问某个Web服务器时,它会将请求发送给Web代…

作者头像 李华
网站建设 2026/3/28 16:47:23

如何快速安装Qt 5.14.2:面向Linux新手的完整指南

如何快速安装Qt 5.14.2&#xff1a;面向Linux新手的完整指南 【免费下载链接】Qt5.14.2开源版Linuxx64安装文件下载 Qt 5.14.2 开源版 Linux x64 安装文件下载 项目地址: https://gitcode.com/Open-source-documentation-tutorial/3ce16 Qt 5.14.2 是一个功能强大的跨平…

作者头像 李华