写点什么

循环中使用 Thread.sleep,代码评审被老板喷了

作者:架构师之道
  • 2025-01-07
    湖南
  • 本文字数:3832 字

    阅读完需:约 13 分钟

场景说明

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

public static boolean 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 进行替换。以下是几种常见的替代方案:

TimerTimerTask

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

代码示例:

import java.util.Timer;import java.util.TimerTask;
public class Main {
    public static boolean 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() 来停止定时器,防止任务继续执行。

优势:

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

缺点:

  • 不支持多线程任务调度:Timer 适用于单线程环境,对于多线程任务调度时,它的表现不如 ScheduledExecutorService

  • 无法处理任务异常:Timer 不能捕捉任务中的异常,任务异常可能导致后续的定时任务停止。

ScheduledExecutorService

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

代码示例:

import java.util.concurrent.*;
public class Main {
    public static boolean 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 秒执行一次任务,直到 FLAGtrue

  • 任务管理:调度器自动管理任务执行,无需显式控制线程的休眠和唤醒,避免了忙等待的问题。

优势:

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

缺点:

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

Object.wait()Object.notify()

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

代码示例:

public class Main {
    private static final Object lock = new Object(); // 用于同步的锁对象    public static boolean 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(),进入等待状态,直到 FLAGtrue 或超时(在此示例中为 3 秒)。

  • Object.notify():当 FLAG 变为 true 时,主线程调用 notify() 唤醒等待的线程。

优势:

  • 线程间协调:利用 wait()notify() 机制,线程可以高效地等待条件的变化,而不需要消耗 CPU 资源。

  • 适用于多线程环境:这种方式适合在多个线程间进行协调和同步,避免了繁琐的手动控制。

缺点:

  • 锁的管理:需要小心避免死锁和竞争条件。线程同步管理相对复杂。

  • 可能的超时问题:在使用 wait() 时,必须谨慎设置超时,否则线程可能会长时间处于阻塞状态。

CompletableFuture

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

代码示例:

import java.util.concurrent.CompletableFuture;import java.util.concurrent.TimeUnit;
public class Main {
    public static boolean 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,一旦 FLAGtrue,就执行 doSomething() 方法。

优势:

  • 异步编程:CompletableFuture 提供了强大的异步任务处理能力,避免了阻塞等待。

  • 链式操作:支持任务的链式调用和回调机制,便于复杂任务的管理。

缺点:

  • 适用场景有限:适合异步任务管理,对于简单的定时任务可能有些复杂。

  • 调试难度:异步任务的调试可能较为复杂,需要更精细的控制。

用户头像

还未添加个人签名 2022-04-10 加入

还未添加个人简介

评论

发布
暂无评论
循环中使用 Thread.sleep,代码评审被老板喷了_编程_架构师之道_InfoQ写作社区