Java - Thread.sleep(0)的意义是什么?

前言

最近看到一篇文章:sleep(0),是关于RocketMQ源码中写的一段代码,说是用于预防GC的。里面还提到了几个关键词:

  • 安全点。
  • 可数/不可数循环。
  • STW
  • navtive方法。

趁此机会,对这方面做一个复习和查缺补漏。官方的源码截图如下:
在这里插入图片描述

一. 安全点和可数循环

我在这篇文章里面,有做过详细的介绍深入理解Java虚拟机系列(二)–垃圾收集器与内存分配策略

这里再来复习一下:

  • Java虚拟机在进行GC的时候,有着STW的特性。即Stop The World,停顿所有Java执行进程。
  • 既然有STW的机制,那么GC是随时发起的吗?并是不,Java虚拟机利用了一个安全点机制Safepoint只有程序到达安全点的时候才能够进行GC

两者结合就是:没有到达安全点,不能够进行GC操作。

那么安全点是如何选定的呢?安全点的选定基本上是以程序 “是否具有让程序长时间执行的特征” 为标准进行选定。例如:

  • 方法调用。
  • 循环跳转。
  • 指令序列复用等代码。

同时安全点的个数还需要适中:

  • 如果安全点太多:导致过于频繁的GC以增加运行时的负荷。
  • 如果安全点太少:导致GC等待时间过长。

那么我们再来说下可数循环这个点。

1.1 可数循环

上面提到了一个安全点的问题。安全点太多导致增加负荷,太少则导致GC等待时间过长。因此,虚拟机针对循环操作,还做了一定的优化:

  • 可数循环:如果循环次数比较少的话,执行时间应该不会太长,使用int类型或者范围更小的数据类型作为索引值,这种是不会被放置安全点的。
  • 不可数循环:反之,如果使用long这样范围更大的类型作为索引的循环,就叫做不可数循环。此时循环过程中就会插入安全点。

也就是说:

  • 可数循环:循环如果没有结束。程序就无法走到安全点,就无法GC
  • 不可数循环:单次循环体结束的时候,就可以进入安全点,无需等待整个循环跑完。

因此这里可以给个小结,如果希望你的程序STW的时间太长,对于一些耗时的循环操作,如果你的索引类型是int,你可以将其改成long

除了这种方式,还有什么方法可以创建出一个安全点呢 ?

1.2 Thread.sleep

Java源码中对于安全点有着相关的注释,在程序进入安全点的时候,Java线程可能处于五种不同的状态:

  • 处于解释执行字节码的状态中。
  • 正在执行native修饰的代码。即JNI调用(Java Native Interface)。
  • 处于编译代码执行中。
  • 线程本身处于blocked状态,比如线程等待锁。
  • 处于转换状态的线程。

在这里,我们关注第二点,执行native修饰的代码。我们看下Thread.sleep的源码:

public class Thread implements Runnable {
	public static native void sleep(long millis) throws InterruptedException;
}

一个线程在运行 native 方法后,返回到 Java 线程后,必须进行一次 safepoint 的检测。

1.3 安全点测试

直接看代码:

public class SafepointTest {
    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 100000000; i++) {
                num.getAndAdd(1);
            }
        };
        Thread t1 = new Thread(runnable, "ThreadOne");
        Thread t2 = new Thread(runnable, "ThreadTwo");
        t1.start();
        t2.start();
        long start = System.currentTimeMillis();
        System.out.println("***********主线程开始睡眠1秒***********");
        Thread.sleep(1000);
        long end = System.currentTimeMillis();
        System.out.println("***********主线程恢复,耗时:+" + (end - start) + "***********");
        System.out.println("最终的计数,num= " + num);
    }
}

这段代码做的事情:

  1. 起两个线程去对num变量进行累加。
  2. 主线程睡眠1秒钟,然后唤醒,输出此时此刻主线程拿到的num是多少。
  3. 理论上,耗时应该和1秒钟差不多。

实际上,输出结果如下:
在这里插入图片描述

我们观测到的结果:主线程等待两个子线程执行完毕之后才输出。中间绝对有一个阻塞的过程。为啥主线程需要等待子线程执行完毕呢?

在这里插入图片描述

我们给代码加一个JVM参数:-XX:+PrintSafepointStatistics,打印安全点的相关日志。结果如下:

在这里插入图片描述
大概意思就是:

  1. JVM想要执行no vm operation,期间有13个线程。正在运行(initially_running)的是2个。
  2. 需要等待这两个线程进入安全点,等待这 2 个线程进入安全点并阻塞耗费了 2231 毫秒。(注意:不同的机器性能,这个结果也不同的)

那么我们再设置一下相关的参数,设置线程进入安全点的超时时间为2000ms(注意要小于你实际运行的时候,真实的大小)

-XX:+SafepointTimeout -XX:SafepointTimeoutDelay=2000

再次运行程序,结果如下:
在这里插入图片描述

这里可以佐证:主线程正在等待ThreadOneThreadTwo两个子线程。那么,为什么主线程应该等待呢?和安全点又有什么关系?

我这里看的是CoderW大佬的文章,总结下就是:

  1. 我们的循环代码,里面用的是int类型作为下标索引,因此他是一个可数循环。只有整个循环执行完毕,才会进入安全点。

  2. JVM 正常运行的时候,如果设置了进入安全点的间隔,就会隔一段时间判断是否有代码缓存要清理,如果有,会进入安全点。

  3. 也就是周期性地停止Java线程,去做缓存清理。这个操作则依赖于安全点。

  4. 而这个间隔,由参数XX:GuaranteedSafepointInterval控制,默认是1秒。

那么我们把GuaranteedSafepointInterval设置为3000会怎样?添加下面参数(顺序也不能错):

-XX:+UnlockDiagnosticVMOptions -XX:GuaranteedSafepointInterval=3000

结果如下:
在这里插入图片描述

同理,如果我们把int i 改成long i,也会有一样的效果:
在这里插入图片描述

二. 总结

首先在总结下上面的案例,对于测试代码,出现主线程等待子线程的情况和安全点有关。

  1. 启动了两个子线程:ThreadOneThreadTwo。并在可数循环做一个耗时的操作。
  2. 此时主线程进入休眠状态1秒钟。
  3. JVM有个参数GuaranteedSafepointInterval默认是1秒,1秒之后,JVM就会尝试在安全点停止,以便让Java线程进行缓存的清理。
  4. 但是对于子线程而言,由于是可数循环,因此内部没有安全点检查。必须等待循环结束。
  5. 那么对于主线程而言,执行了sleep方法,由于它是native修饰的,因此返回到 Java 线程后,必须进行一次 safepoint 的检测,进入安全点。
  6. 主线程发现安全点操作正在进行中(得等待子线程执行完毕,此时还没有安全点),于是把自己挂起,直到操作结束。
  7. 那么对于我们而言,观察的结果就是:主线程等待子线程循环完毕才输出。

因此当我们GuaranteedSafepointInterval修改为3000秒的时候(生产环境切忌不要随意改动),由于时间间隔大于循环所耗的时间,因此不会进入安全点检查。因此主线程睡眠1秒之后,发现没有安全点,因此可以正常的走后续流程。

例如,除了上文的改参数、改int类型,还可以这么改:

public class SafepointTest {
    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 100000000; i++) {
                if (i % 1000 == 0) {
                    try {
                        Thread.sleep(0);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                num.getAndAdd(1);
            }
        };
        Thread t1 = new Thread(runnable, "ThreadOne");
        Thread t2 = new Thread(runnable, "ThreadTwo");
        t1.start();
        t2.start();
        long start = System.currentTimeMillis();
        System.out.println("***********主线程开始睡眠1秒***********");
        Thread.sleep(1000);
        long end = System.currentTimeMillis();
        System.out.println("***********主线程恢复,耗时:+" + (end - start) + "***********");
        System.out.println("最终的计数,num= " + num);
    }
}

那么结合案例,Thread.sleep这个方法。他的作用就显而易见了:

  1. 你可以理解为执行了它,就相当于插入了一个安全点。
  2. 这样就可以避免你的程序 GC 线程长时间等待。 也就是所谓的GC削峰。
### 回答1: 在 Java 中,除了使用 Thread.sleep() 方法来使线程暂停一段时间外,还可以使用以下方法进行替代: 1. Object.wait(long timeout):使当前线程等待一段时间,或者直到其他线程调用该对象的 notify() 或 notifyAll() 方法。这个方法需要在 synchronized 块中使用,并且线程会释放锁。 2. TimeUnit.SECONDS.sleep(long seconds):使用 TimeUnit 类的 sleep() 方法,可以更方便地指定休眠的时间单位。 3. ScheduledExecutorService.schedule(Runnable command, long delay, TimeUnit unit):使用 ScheduledExecutorService 接口的 schedule() 方法,可以在一定时间后执行任务。 注意:使用以上方法替代 Thread.sleep() 时,需要做好异常处理。 ### 回答2: 在Java中,Thread.sleep()方法可以暂停当前线程的执行一段时间。然而,有时候我们需要在不使用Thread.sleep()的情况下实现类似的效果,可以考虑使用以下方法替代: 1. 使用java.util.concurrent包下的DelayQueue类。DelayQueue是一个无界阻塞队列,可以实现延迟执行任务的效果。我们可以创建一个DelayQueue对象,然后将需要延迟执行的任务封装成实现Delayed接口的对象,设置好延迟时间,然后将任务添加到DelayQueue中。后续线程可以从DelayQueue中获取任务执行。 2. 使用java.util.concurrent包下的ScheduledExecutorService接口。ScheduledExecutorService是一个可定时调度执行任务的服务。我们可以使用ScheduledExecutorService的schedule()方法或者scheduleAtFixedRate()方法来替代Thread.sleep()。这些方法中的参数可以指定任务的执行时间或者执行周期。 3. 使用java.lang.Object类的wait()方法和notify()/notifyAll()方法。这是Java中用于线程之间通信的机制。我们可以在一个线程里使用wait()方法让其进入等待状态,然后在其他线程中使用notify()/notifyAll()方法唤醒该线程。通过这种方式,我们可以实现线程的阻塞和唤醒操作,达到替代Thread.sleep()的效果。 综上所述,我们可以根据具体需求选择适合的替代方法来实现替代Thread.sleep()的功能。 ### 回答3: 在Java中,除了使用Thread.sleep()方法外,还可以使用其他方式来实现类似的效果。 1. 使用ScheduledExecutorService定时器: 可以使用ScheduledExecutorService定时器来替代Thread.sleep(),这样更灵活和可控。通过创建一个定时任务,可以指定任务的延迟时间,然后执行需要暂停的任务。示例代码如下: ```java import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class SleepAlternative { public static void main(String[] args) { ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); executorService.schedule(() -> { // 执行需要暂停的任务 try { Thread.sleep(1000); // 暂停1秒 System.out.println("任务执行完毕"); } catch (InterruptedException e) { e.printStackTrace(); } }, 1, TimeUnit.SECONDS); executorService.shutdown(); } } ``` 2. 使用Object的wait()和notify()方法: 通过使用Object类的wait()和notify()方法,可以实现线程的等待和唤醒,从而达到类似Thread.sleep()的效果。示例代码如下: ```java public class SleepAlternative { public static void main(String[] args) { Object lock = new Object(); Thread thread = new Thread(() -> { synchronized (lock) { try { lock.wait(1000); // 暂停1秒 System.out.println("任务执行完毕"); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); } } ``` 通过上述两种方式,可以实现线程的等待和暂停,从而达到替代Thread.sleep()的效果。这些替代方法相对于Thread.sleep()更加灵活和可控,同时可以避免一些潜在的问题,如线程被中断等。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值