逐暗者的麦田 一位Java攻城狮的个人博客,主要分享编程、建站、动漫、趣闻和生活内容
博主 逐暗者的麦田

本站由 又拍云又拍云提供CDN加速/云存储服务

萌ICP备20237379号沪ICP备:13037081号-2,13037081号-1,13037081号-3 博主 昨天 22:48 在线自豪地使用 Typecho 建站搭配使用 🌻Sunny 主题当前在线 3 人
歌曲封面 未知作品

本站由 又拍云又拍云提供CDN加速/云存储服务

萌ICP备20237379号

沪ICP备:13037081号-2,13037081号-1,13037081号-3

网站已运行 3 年 58 天 9 小时 20 分

Powered by Typecho & Sunny

3 online · 31 ms

Title

Thread.sleep(0)的奇怪现象

逐暗者

·

程序人生

·

Article
⚠️ 本文最后更新于2023年08月05日,已经过了444天没有更新,若内容或图片失效,请留言反馈

背景

先来看这么一段代码

♾️ 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)的技术来更进一步地平衡安全点检查对吞吐量和延迟所产生的影响。就不会产生第一段代码的效果了。

现在已有 955 次阅读,0 条评论,1 人点赞
Comment:共0条
发表
搜 索 消 息 足 迹
你还不曾留言过..
你还不曾留下足迹..
博主 不再显示
博主