网站LOGO
逐暗者的麦田
页面加载中
4月26日
网站LOGO 逐暗者的麦田
一个java软件攻城狮
菜单
  • 逐暗者的麦田
    一个java软件攻城狮
    用户的头像
    首次访问
    上次留言
    累计留言
    我的等级
    我的角色
    打赏二维码
    打赏博主
    Thread.sleep(0)的奇怪现象
    点击复制本页信息
    微信扫一扫
    文章二维码
    文章图片 文章标题
    创建时间
  • 一 言
    确认删除此评论么? 确认
  • 本弹窗介绍内容来自,本网站不对其中内容负责。
    按住ctrl可打开默认菜单

    Thread.sleep(0)的奇怪现象

    shellingford · 原创 ·
    程序人生 · 安全点
    共 3374 字 · 约 3 分钟 · 389
    本文最后更新于2023年08月05日,已经过了265天没有更新,若内容或图片失效,请留言反馈

    背景

    先来看这么一段代码

    java 代码:
    public class Test {
    
        public static AtomicInteger num = new AtomicInteger(0);
    
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = () -> {
                for (int i = 0; i < 1000000000; i++) {
                    num.getAndAdd(1);
                }
                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 = " + num);
        }
    
    }

    这段代码启动了2个线程,然后运行了很多次计数器加1操作,主线程等待1000毫秒后就打印。看上去无论2个线程是否长时间运行,主线程都应该在1秒后打印num的值。
    但奇怪的现象是主线程会等待2个子线程结束后再打印sleep被延长了很久。


    当然这个现象只在jdk10以下的版本会出现,从jdk10开始就不会有这个现象了。

    sleep(0)

    现在修改一下这段代码,增加一个sleep(0)看看会有什么变化

    java 代码:
    public class Test {
    
        public static AtomicInteger num = new AtomicInteger(0);
    
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = () -> {
                for (int i = 0; i < 1000000000; i++) {
                    num.getAndAdd(1);
                    if(i % 100 == 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 = " + num);
        }
    }

    一切都变得正常了,主线程1秒后就打印了数值,当然由于2个线程并未执行完,所以num值并不是2000000000 。

    安全点

    之所以会有这样的差异,主要是因为sleep会进入安全点。

    关于安全点的描述,我们可以看看《深入理解JVM虚拟机(第三版)》的 3.4.2 小节:

    在HotSpot中,JVM {% kbd Stop-the-World %} 的暂停机制称为安全点。在安全点期间,所有运行java代码的线程都被挂起。运行native代码的线程可以继续运行,只要它们不与JVM交互(比如尝试通过JNI访问Java对象,调用Java方法或从native返回Java,这些将挂起线程直到安全点结束)。

    启动安全点的原因

    以下是 HotSpot JVM 启动安全点的几个原因:

    • 垃圾收集暂停
    • 代码去优化
    • 刷新代码缓存
    • 类重新定义(例如热插拔或仪表)
    • 偏向锁撤销
    • 各种调试操作(例如死锁检查或堆栈跟踪转储)

    安全点的实现

    1.安全点的检查就是在代码的某些位置插入了些安全点的检查代码
    2.应用线程到达安全点时,是否需要进入安全点状态,通常只是个状态判定,比如线程是否被标记为poll armed,或者本地 local polling page 是否为脏,是否已经在block状态(已经block的,安全点未结束前,不离开block状态)等
    3.应用线程如果发现需要进入安全点状态,则并把自己状态改为安全(把自己阻塞block)
    4.需要所有应用线程都到达安全点安全状态(block阻塞状态),才开始做安全点的动作。

    • 安全点设置的优化

    在循环中如果插入过多的安全点,则会导致效率低下,所以在可以预见不会太长的循环体中不插入安全点。而使用int类型变量进行for循环就是这样的一个被认为不会太长的循环。

    所以第一段代码中,之所以会出现主线程等待子线程的情况,是因为子线程在进入循环体内后没有安全点。而第二段代码中显然sleep(0)的添加增加了安全点。

    如何进入安全点

    jdk8的safepoint说明

    其中Running in native code 中说明了,线程从native方法退回到jvm虚拟机中时需要进行安全点检查。而sleep这个方法正好就是native方法。

    其他

    除了采用sleep(0)来使线程进入安全点外,还能使用long代替int进行for循环。long被认为不是一个短暂的循环体,会被插入安全点检查。

    另外,Java 10引入了一项更高级的叫循环切分(loop strip mining)的技术来更进一步地平衡安全点检查对吞吐量和延迟所产生的影响。就不会产生第一段代码的效果了。

    声明:本文由 shellingford(博主)原创,依据 CC-BY-NC-SA 4.0 许可协议 授权,转载请注明出处。

    还没有人喜爱这篇文章呢

    发一条! 发一条!
    博客logo 逐暗者的麦田 一个java软件攻城狮
    MOEICP 萌ICP备20237379号 ICP 沪ICP备13037081号-2,沪ICP备13037081号-1,沪ICP备13037081号-3 又拍云 本站由又拍云提供CDN加速/云存储服务

    🕛

    本站已运行 2 年 244 天 11 小时 44 分

    🌳

    自豪地使用 Typecho 建站,并搭配 MyLife 主题
    逐暗者的麦田. © 2021 ~ 2024.
    网站logo

    逐暗者的麦田 一个java软件攻城狮
     
     
     
     
    壁纸