⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 飞天小牛肉 「小牛肉」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

场景说明

假设我们有以下简单代码,旨在每隔 3 秒检查一次标记量 FLAG,如果 FLAG 为 true,则执行某些操作:

public staticboolean FLAG = false;

public static void main(String[] args) throws InterruptedException {
while (true) {
Thread.sleep(3000); // 每隔3秒检查一次标记量
if (FLAG) {
doSomething();
break;
}
}
}

public static void doSomething() {
System.out.println("Hello World!!!");
}

在这段代码中,使用 Thread.sleep(3000) 来避免频繁检查标记量的值,看起来非常合理对吧。然而,这种做法会导致性能问题,IDEA 会给出警告:

Call to ‘Thread.sleep()’ in a loop, probably busy-waiting

在循环中调用 Thread.sleep(),可能会导致 busy-waiting(忙等待)

原因分析

调用 Thread.sleep() 时,操作系统需要对线程进行挂起和唤醒操作,每次休眠后线程都需要被恢复到运行状态。这种频繁的线程状态切换会导致严重的性能开销,特别是在短时间间隔内(如 3 秒)。这不仅会浪费 CPU 资源,还可能导致应用的响应延迟增加,甚至在高负载的情况下引发性能瓶颈。

解决方案

如果对文中示例有所了解,你会发现本质其实就是一个定时问题,每隔一段时间进行操作直至达到条件,按文中示例,便是每隔 3 秒检查标记量并做一些事情,因此我们完全可使用调度 API 进行替换。以下是几种常见的替代方案:

Timer 和 TimerTask

Timer 和 TimerTask 提供了简单的定时任务调度机制,是 Java 中用于调度定时任务的老牌工具。虽然在多线程环境下并不如 ScheduledExecutorService 强大,但它仍然是一种简单有效的定时任务解决方案。

代码示例:

import java.util.Timer;
import java.util.TimerTask;

publicclass Main {

publicstaticboolean FLAG = false;

public static void main(String[] args) {

// 创建定时器
Timer timer = new Timer();

// 定时任务,每隔3秒检查一次
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {

解析:

  • Timer.scheduleAtFixedRate():该方法能够以固定频率调度任务。在本例中,我们设置了每 3 秒执行一次任务。
  • 取消定时任务:任务完成后,我们调用 timer.cancel() 来停止定时器,防止任务继续执行。

优势:

  • 简洁易用:Timer 和 TimerTask 的使用简单,适用于轻量级的定时任务。

缺点:

  • 不支持多线程任务调度:Timer 适用于单线程环境,对于多线程任务调度时,它的表现不如 ScheduledExecutorService。
  • 无法处理任务异常:Timer 不能捕捉任务中的异常,任务异常可能导致后续的定时任务停止。

ScheduledExecutorService

ScheduledExecutorService 是 Java 5 引入的一个更为现代的调度接口,它相较于 Timer 更加灵活和强大,能够更好地处理多线程任务。

代码示例:

import java.util.concurrent.*;

publicclass Main {

publicstaticboolean FLAG = false;

public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

scheduler.scheduleAtFixedRate(() -> {
if (FLAG) {
doSomething();
scheduler.shutdown(); // 任务完成后关闭调度器
}
}, 0, 3, TimeUnit.SECONDS); // 延迟0秒后每3秒执行一次
}

public static void doSomething() {
System.out.println("Hello World!!!");
}
}

解析:

  • ScheduledExecutorService:该接口支持定时任务的调度,可以避免频繁的线程上下文切换。通过 scheduleAtFixedRate() 方法,每 3 秒执行一次任务,直到 FLAG 为 true。
  • 任务管理:调度器自动管理任务执行,无需显式控制线程的休眠和唤醒,避免了忙等待的问题。

优势:

  • 多线程支持:ScheduledExecutorService 适用于多线程环境,并且线程池的使用能够有效减少线程创建和销毁的开销。

缺点:

  • 适用性:相比 Timer,ScheduledExecutorService 在代码上稍显复杂,尤其在简单的定时任务中,可能稍显复杂。

Object.wait() 和 Object.notify()

在多线程环境中,可以使用 Object.wait() 和 Object.notify() 方法来进行线程间的协调。通过这种方式,线程可以等待特定条件的变化,而无需频繁轮询和消耗 CPU 资源。

代码示例:

public class Main {

privatestaticfinal Object lock = new Object(); // 用于同步的锁对象
publicstaticboolean FLAG = false;

public static void main(String[] args) throws InterruptedException {

// 创建并启动检查线程
Thread checkerThread = new Thread(() -> {
synchronized (lock) {
while (!FLAG) {
try {
lock.wait(3000); // 等待3秒或 FLAG 被设置为 true
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
doSomething();
}
});

checkerThread.start();

// 模拟一些其他工作,最后设置 FLAG 为 true
Thread.sleep(5000); // 等待5秒钟
FLAG = true;

// 通知等待的线程可以继续执行
synchronized (lock) {
lock.notify(); // 唤醒等待线程
}
}

public static void doSomething() {
System.out.println("Hello World!!!");
}
}

解析:

  • Object.wait():线程在 lock 对象上调用 wait(),进入等待状态,直到 FLAG 为 true 或超时(在此示例中为 3 秒)。
  • Object.notify():当 FLAG 变为 true 时,主线程调用 notify() 唤醒等待的线程。

优势:

  • 线程间协调:利用 wait() 和 notify() 机制,线程可以高效地等待条件的变化,而不需要消耗 CPU 资源。
  • 适用于多线程环境:这种方式适合在多个线程间进行协调和同步,避免了繁琐的手动控制。

缺点:

  • 锁的管理:需要小心避免死锁和竞争条件。线程同步管理相对复杂。
  • 可能的超时问题:在使用 wait() 时,必须谨慎设置超时,否则线程可能会长时间处于阻塞状态。

CompletableFuture

CompletableFuture 是 Java 8 引入的一个强大的异步编程工具。它可以方便地处理异步任务,并允许在任务完成时触发回调,避免阻塞等待。

代码示例:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

publicclass Main {

publicstaticboolean FLAG = false;

public static void main(String[] args) {

// 异步检查 FLAG 并执行操作
CompletableFuture.runAsync(() -> {
while (!FLAG) {
try {
TimeUnit.SECONDS.sleep(3); // 每隔3秒检查一次
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
doSomething();
});

// 模拟一些工作,稍后设置 FLAG 为 true
try {
TimeUnit.SECONDS.sleep(5); // 模拟延迟
FLAG = true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

public static void doSomething() {
System.out.println("Hello World!!!");
}
}

解析:

  • CompletableFuture.runAsync():在后台线程中异步执行任务,检查 FLAG 的状态并执行操作。
  • 异步控制:任务会每 3 秒检查一次 FLAG,一旦 FLAG 为 true,就执行 doSomething() 方法。

优势:

  • 异步编程:CompletableFuture 提供了强大的异步任务处理能力,避免了阻塞等待。
  • 链式操作:支持任务的链式调用和回调机制,便于复杂任务的管理。

缺点:

  • 适用场景有限:适合异步任务管理,对于简单的定时任务可能有些复杂。
  • 调试难度:异步任务的调试可能较为复杂,需要更精细的控制。
文章目录
  1. 1. 场景说明
  2. 2. 原因分析
  3. 3. 解决方案
    1. 3.1. Timer 和 TimerTask
    2. 3.2. ScheduledExecutorService
    3. 3.3. Object.wait() 和 Object.notify()
    4. 3.4. CompletableFuture