加载中...
Thread.sleep(0)的奇怪现象
发表于:2022-09-13 | 分类: 程序人生
字数统计: 1.1k | 阅读时长: 4分钟 | 阅读量:

背景


先来看这么一段代码

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)看看会有什么变化

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

上一篇:
一次性讲清楚voliate的可见性
下一篇:
用树莓派加内网穿透搭免费网站
本文目录
本文目录