草庐IT

深入理解Thread.sleep(1000)的注意事项及原理分析

月夜烛峰 2023-10-30 原文

目录

一、前言

二、什么是SWT

三、代码示例

1、Thread.sleep(500)

2、 Thread.sleep(1000)

四、原因分析

1、安全点(Safepoint)

2、源码分析

3、JVM参数

4、GC日志抓取 

5、JVM源码定位

五、int与long


一、前言

最近服务器上出现了一次长时间的STW,原因也比较诡异.

通过jstack分析,可疑代码居然是Thread.sleep(1000)

通过测试代码模拟,发现确实如此:

"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x000000012700e800 nid=0x4d03 in Object.wait() [0x000000016f036000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000715586c08> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x0000000715586c08> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"main" #1 prio=5 os_prio=31 tid=0x000000013700c800 nid=0x2903 sleeping[0x000000016d9b2000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at com.zhufeng.gc.TestSavePoint.main(TestSavePoint.java:29)

"VM Thread" os_prio=31 tid=0x0000000126847000 nid=0x3303 runnable

其实在使用定时任务、心跳检测或者批量处理时,经常用线程来处理,并添加Thread.sleep(xxx)来进行时间控制,趁着中秋假期,花了点时间研究了一下Thread.sleep(xxx)的相关知识,果然不是平时使用的这么简单。

二、什么是SWT

本篇设计的内容比较深入,为了更好的理解,先介绍下什么是SWT。

STW: Stop-The-World, 是在垃圾回收算法执⾏过程当中,将JVM内存冻结丶应用程序停顿的⼀种状态。

如果系统卡顿很明显,大概率就是频繁执行GC垃圾回收,频繁进入STW状态产生停顿的缘故

  • 在STW 状态下,JAVA的所有线程都是停⽌执⾏的 -> GC线程除外
  • 一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。
  • STW是不可避免的,垃圾回收算法执⾏一定会出现STW,我们要做的只是减少停顿的时间
  • GC各种算法优化的重点,就是减少STW(暂停),同时这也是JVM调优的重点。

三、代码示例

先通过测试代码,演示下Thread.sleep()导致的问题,然后再分析原因。

1、Thread.sleep(500)

/**
 * @ClassName: TestSavePoint
 * @Description GC 安全点测试
 * @author 月夜烛峰
 * @date 2022/9/9 14:21
 */
public class TestSavePoint {

    static AtomicInteger atomicInteger = new AtomicInteger(0);

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

        Runnable runnable = () -> {
            for (int i = 0; i < 500000000; i++) {
                atomicInteger.incrementAndGet();
            }
            System.out.println(Thread.currentThread().getName() + " 执行结束...");
        };
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);

        t1.start();
        t2.start();
        Thread.sleep(500);
        System.out.println("num="+atomicInteger);

    }
}

在main方法中启动两个线程t1和t2,两个线程对AtomicInteger对象进行递增,因为AtomicInteger通过CAS机制保证了原子性,所以在两个线程并发循环中,AtomicInteger递增不会有影响。

上面代码经过测试,for循环5亿次,需要8秒左右的时间,所以应该会先打印:

System.out.println("num="+atomicInteger);

实际运行下:

运行结果符合预期,然后修改代码,将Thread.sleep(500)改为Thread.sleep(1000)

2、 Thread.sleep(1000)

通过上图动态执行结果可以看到,System.out.println("num="+atomicInteger) 等到两个线程执行完成后才执行,期间已经过去了几秒钟,而且打印结果为t1和t2循环次数的总和。Thread.sleep(1000)好像没有生效,而且长时间的停顿是如何出现的?

四、原因分析

1、安全点(Safepoint)

在《深入理解JVM虚拟机(第三版)》5.2.8 小节提到:由安全点导致长时间停顿

是HotSpot虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行时间应该也不会太长,所以使用int类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环(Counted Loop),相对应地,使用long或者范围更大的数据类型作为索引值的循环就被称为不可数循环(Uncounted Loop),将会被放置安全点。

安全点又是什么?

2、源码分析

看下Safepoint.cpp源码,通过注释可以了解,Java线程可能处于几种不同的状态,不同的状态提供了几种不同的处理方案。

第二点内容最多,有可能是重点,所以简单梳理第一点后,从第二点开始重点分析。

When returning from the native code, a Java thread must check the safepoint _state to see if we must block.

看到第二点的第一句话中有一个must,意思就是一个线程在运行 native 方法后,返回到 Java 线程后,一定会进行 safepoint 的检测。

想起开篇测试代码中奇怪的现象是由Thread.sleep(xxx) 导致,所以先看下Thread.sleep(xxx)的源码:

心中不免有一丝兴奋,果然有个native! 

3、JVM参数

经过理论分析,我们断定肯定因为Thread.sleep(1000)进入safepoints,然后导致所有线程停止运行。

通过官网可知:

HotSpot JVM调用 safepoints 周期由 -XX:GuaranteedSafepointInterval 选项控制,每经过-XX:GuaranteedSafepointInterval 配置的时间,都会让所有线程进入 Safepoint,该选项默认为 1000ms。

安全点JVM默认值查看命令:

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal 2>&1 | grep Safepoint

接下来开始抓去GC日志信息进行验证。

4、GC日志抓取 

运行测试代码时,添加VM参数:-XX:GuaranteedSafepointInterval  

运行代码:

说明:

JVM 想执行 「no vm operation」 ,这个操作需要线程都进入安全点,整个期间一共有 12 个线程,正在运行的线程有 2 个,需要等待这两个线程进入安全点,等待这 2 个线程进入安全点并阻塞耗费了 7285 毫秒

查看正在运行的线程:

添加VM参数:

-XX:+SafepointTimeout -XX:SafepointTimeoutDelay=7000

说明:

7000为超时时间

正在运行的线程为Thread-1和Thread-0,为了更好的区分可以修改下线程名称重新打印:

Thread t1 = new Thread(runnable,"线程一");
Thread t2 = new Thread(runnable,"线程二");

5、JVM源码定位

抓区GC日志时,显示执行「no vm operation」操作时进入了安全点,那么什么是no vm operation? 

在HotSpot代码中搜索:no vm operation

搜索结果只有几个,可以很容易找到日志输出的方法。

 sstats->_vmop_type == -1 ? "no vm operation" :
               VM_Operation::name(sstats->_vmop_type)

这里是一个三目运算,当_vmop_type不等于-1时,才会输出no vm operation

  typedef struct {
    float  _time_stamp;                        // record when the current safepoint occurs in seconds
    int    _vmop_type;                         // type of VM operation triggers the safepoint
    int    _nof_total_threads;                 // total number of Java threads
    int    _nof_initial_running_threads;       // total number of initially seen running threads
    int    _nof_threads_wait_to_block;         // total number of threads waiting for to block
    bool   _page_armed;                        // true if polling page is armed, false otherwise
    int    _nof_threads_hit_page_trap;         // total number of threads hitting the page trap
    jlong  _time_to_spin;                      // total time in millis spent in spinning
    jlong  _time_to_wait_to_block;             // total time in millis spent in waiting for to block
    jlong  _time_to_do_cleanups;               // total time in millis spent in performing cleanups
    jlong  _time_to_sync;                      // total time in millis spent in getting to _synchronized
    jlong  _time_to_exec_vmop;                 // total time in millis spent in vm operation itself
  } SafepointStats;

通过代码注释可以了解_vmop_type只是一种可以触发安全点的类型,继续在代码里搜索有关_vmop_type的赋值逻辑:

在这里搜到了计数统计,通过方法名就可以知判断,这个方法作为安全点调用的统计方法,当然是在触发安全点调用的。

void SafepointSynchronize::begin_statistics(int nof_threads, int nof_running) {...}

begin_statistics这里加了一个statistics的后缀,说明肯定有配套的begin方法,顺着这个思路搜索:SafepointSynchronize::begin() 

当出现vmThread.cpp的时候,应该是找对路子了:

loop()方法里列举了几种VM操作,从标注的三个地方可以看出是进入安全点的主要判断逻辑。

包括:线程是否正常运行,时间点是否设置,以及缓存处理。

经过以上分析,进入安全点,会导致:

  • 主线程进入睡眠状态 1 秒钟。
  • 在1000 ms(GuaranteedSafepointInterval)之后,JVM尝试在安全点停止,以便Java线程进行定期清理,但是直到计数循环完成后才能执行此操作。
  • Thread.sleep 方法从 native 返回,发现安全点操作正在进行中,于是把自己挂起,直到操作结束。

但是还有一个很大的疑问,JVM默认参数设置下,等间隔一秒中后,会进入安全点,但是为什么主线程的结果打印一直等待其它线程运行结束后才会运行?

五、int与long

我们再回头看第四节的第1点:

是HotSpot虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行时间应该也不会太长,所以使用int类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环(Counted Loop),相对应地,使用long或者范围更大的数据类型作为索引值的循环就被称为不可数循环(Uncounted Loop),将会被放置安全点。

简单讲,也就是int被虚拟机认为比较小,使用int循环时,会等到循环结束后进入安全点,这也就说明了为什么主线程的打印信息在Thread.sleep(1000)时要等到其它两个线程处理完成后才会打印。

看下使用long 打印:

主线程并没有等到其它两个线程结束后,再打印,与我们的预期相符。

RocketMQ的作者在MQ中使用了这样一种处理,Thread.sleep(0)。

这是为了避免循环较大时,最后进入安全点导致性能问题,因此我们也可以修改为如下方式: 

/**
 * @ClassName: TestSavePoint
 * @Description GC 安全点测试
 * @author 月夜烛峰
 * @date 2022/9/9 14:21
 */
public class TestSavePoint {

    static AtomicInteger atomicInteger = new AtomicInteger(0);

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

        Runnable runnable = () -> {
            for (int i = 0; i < 500000000; i++) {
                atomicInteger.incrementAndGet();
                if(i%1000==0){
                    try {
                        Thread.sleep(0);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println(Thread.currentThread().getName() + " 执行结束...");
        };
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);

        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("num="+atomicInteger);

    }
}

Thread.sleep(1000)的问题,果然解决。

有关深入理解Thread.sleep(1000)的注意事项及原理分析的更多相关文章

  1. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  2. 阿里云国际版免费试用:如何注册以及注意事项 - 2

    作为新的阿里云用户,您可以50免费试用多种优惠,价值高达1,700美元(或8,500美元)。这将让您了解和体验阿里云平台上提供的一系列产品和服务。如果您以个人身份注册免费试用,您将获得价值1,700美元的优惠。但是,如果您是注册公司,您可以选择企业免费试用,提交基本信息通过企业实名注册验证,即可开始价值$8,500的免费试用!本教程介绍了如何设置您的帐户并使用您的免费试用版。​关于免费试用在我们开始此试用之前,您还必须遵守以下条款和条件才能访问您的免费试用:只有在一年内创建的账户才有资格获得阿里云免费试用。通过此免费试用优惠,用户可以免费试用免费试用活动页面上列出的每种产品一次。如果您有多个帐

  3. TimeSformer:抛弃CNN的Transformer视频理解框架 - 2

    Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图

  4. ruby - 易于初学者理解的 Ruby 库 - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。关闭3年前。Improvethisquestion我正处于学习Ruby的阶段,我想查看一些小型库的源代码以了解它们是如何构建的。我不知道什么是小型图书馆,但希望SO能推荐一些易于理解的图书馆来学习。因此,如果有人知道一两个非常小的库,这是新手Rubyists学习的好例子,请推荐!我想使用Manveru'sInnatelib,因为它试图保持在2000LOC以下,但我还不熟悉其中经常使用的Ruby速记。也许大约100-5

  5. ruby - 无法理解 `puts{}.class` 和 `puts({}.class)` 之间的区别 - 2

    由于匿名block和散列block看起来大致相同。我正在玩它。我做了一些严肃的观察,如下所示:{}.class#=>Hash好的,这很酷。空block被视为Hash。print{}.class#=>NilClassputs{}.class#=>NilClass为什么上面的代码和NilClass一样,下面的代码又显示了Hash?puts({}.class)#Hash#=>nilprint({}.class)#Hash=>nil谁能帮我理解上面发生了什么?我完全不同意@Lindydancer的观点你如何解释下面几行:print{}.class#NilClassprint[].class#A

  6. ruby - 如何理解 Ruby 中的发送者和接收者? - 2

    我很难理解Ruby中sender和receiver的实际含义。它们一般是什么意思?到目前为止,我只是将它们理解为方法调用和获取其返回值的调用。但是,我知道我的理解还远远不够。谁能给我一个Ruby中发送者和接收者的具体解释? 最佳答案 面向对象中的一个核心概念是消息传递和早期概念化,这在很大程度上借鉴了计算的Actor模型。艾伦·凯(AlanKay)创造了面向对象一词并发明了最早的OO语言之一SmallTalk,他拥有voicedregretatusingatermwhichputthefocusonobjectsinsteadofo

  7. ruby-on-rails - Rails - 理解 application.js 和 application.css - 2

    rails新手。只是想了解\assests目录中的这两个文件。例如,application.js文件有如下行://=requirejquery//=requirejquery_ujs//=require_tree.我理解require_tree。只是将所有JS文件添加到当前目录中。根据上下文,我可以看出requirejquery添加了jQuery库。但是它从哪里得到这些jQuery库呢?我没有在我的Assets文件夹中看到任何jquery.js文件——或者直接在我的整个应用程序中没有看到任何jquery.js文件?同样,我正在按照一些说明安装TwitterBootstrap(http:

  8. ruby - Watir ... sleep 和等待之间的区别 - 2

    有什么显着的区别吗sleep10和wait_until(10)他们似乎都在做同样的事情:WAITING10秒,然后继续下一步 最佳答案 sleep在指定时间内什么都不做。wait_untiltakesablock.它一直等到block评估为真或超时。如果没有给出block,它们的行为相同。 关于ruby-Watir...sleep和等待之间的区别,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/que

  9. 建模分析 | 平面2R机器人(二连杆)运动学与动力学建模(附Matlab仿真) - 2

    目录0专栏介绍1平面2R机器人概述2运动学建模2.1正运动学模型2.2逆运动学模型2.3机器人运动学仿真3动力学建模3.1计算动能3.2势能计算与动力学方程3.3动力学仿真0专栏介绍?附C++/Python/Matlab全套代码?课程设计、毕业设计、创新竞赛必备!详细介绍全局规划(图搜索、采样法、智能算法等);局部规划(DWA、APF等);曲线优化(贝塞尔曲线、B样条曲线等)。?详情:图解自动驾驶中的运动规划(MotionPlanning),附几十种规划算法1平面2R机器人概述如图1所示为本文的研究本体——平面2R机器人。对参数进行如下定义:机器人广义坐标

  10. 网站日志分析软件--让网站日志分析工作变得更简单 - 2

    网站的日志分析,是seo优化不可忽视的一门功课,但网站越大,每天产生的日志就越大,大站一天都可以产生几个G的网站日志,如果光靠肉眼去分析,那可能看到猴年马月都看不完,因此借助网站日志分析工具去分析网站日志,那将会使网站日志分析工作变得更简单。下面推荐两款网站日志分析软件。第一款:逆火网站日志分析器逆火网站日志分析器是一款功能全面的网站服务器日志分析软件。通过分析网站的日志文件,不仅能够精准的知道网站的访问量、网站的访问来源,网站的广告点击,访客的地区统计,搜索引擎关键字查询等,还能够一次性分析多个网站的日志文件,让你轻松管理网站。逆火网站日志分析器下载地址:https://pan.baidu.

随机推荐