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

科技   2025-01-09 17:00   江苏  
将 脚本之家 设为“星标

第一时间收到文章更新

来源公众号:飞天小牛肉  ID:CS-Wiki

场景说明

假设我们有以下简单代码,旨在每隔 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();  // 任务完成后关闭调度器
            }
        }, 03, 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 提供了强大的异步任务处理能力,避免了阻塞等待。
  • 链式操作:支持任务的链式调用和回调机制,便于复杂任务的管理。

缺点:

  • 适用场景有限:适合异步任务管理,对于简单的定时任务可能有些复杂。
  • 调试难度:异步任务的调试可能较为复杂,需要更精细的控制。




  推荐阅读:
  1. 用Rust重写近6万行C++代码是怎样的体验?
  2. 华为OD面试:三个线程交替打印ABC如何实现?
  3. 代码质量堪忧!Win11升级弹窗自己卡死崩溃
  4. 写了一个分页 sql,因为粗心出了 bug 造成了 OOM!

  5. 电话普及二十年后,年轻人开始害怕接电话。

脚本之家
脚本之家(jb51.net)每天提供最新IT类资讯、原创内容、编程开发的教程与经验分享,送书福利天天在等你!
 最新文章