JAVA Thread.sleep(0)深入理解

JAVA并发-Thread.sleep(0)深入理解

Thread.Sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争”。

通过调用 Thread.sleep(0) 的目的是为了让 GC 线程有机会被操作系统选中,从而进行垃圾清理的工作。它的副作用是,可能会更频繁地运行 GC,毕竟你每 1000 次迭代就有一次运行 GC 的机会,但是好处是可以防止长时间的垃圾收集。

不是 prevent gc,而是对 gc 采取了“打散运行,削峰填谷”的思想,从而 prevent long time gc。

安全点 safepoint

安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。

没有到安全点,是不能 STW,从而进行 GC 的。

安全点太少,让收集器等待时间过长,过于频繁,会增大运行时内存负荷。

程序不可能因为指令流太长而长时间运行,长时间代码运行,最明显特征就是指令序列复用,例如方法调用,循环跳转,异常跳转,只有具有这些功能的指令才会产生安全点。

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

调用 sleep 方法的线程会进入 Safepoint。

不可数循环 Uncounted Loop

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

意思就是在可数循环(Counted Loop)的情况下,HotSpot 虚拟机搞了一个优化,就是等循环结束之后,线程才会进入安全点。

反过来说就是:循环如果没有结束,线程不会进入安全点,GC 线程就得等着当前的线程循环结束,进入安全点,才能开始工作。

int遍历,可数循环 Counted Loop

@Slf4j
public class MainTest {
    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        log.info("begin:");
        Runnable runnable = () -> {
            for (int i = 0; i < 1000000000; i++) {
                num.getAndAdd(1);
            }
            log.info(Thread.currentThread().getName() + "执行结束!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        log.info("end:num = " + num);
    }
}

Output:

13:25:44.516 [main] INFO com.qhong.basic.safepoint.MainTest - begin:
13:26:32.385 [Thread-1] INFO com.qhong.basic.safepoint.MainTest - Thread-1执行结束!
13:26:32.385 [Thread-0] INFO com.qhong.basic.safepoint.MainTest - Thread-0执行结束!
13:26:32.385 [main] INFO com.qhong.basic.safepoint.MainTest - end:num = 2000000000

开始以为主线程休眠 1000ms 后就会输出结果,但是实际情况却是主线程一直在等待 t1,t2 执行结束才继续执行。

这个循环就属于前面说的可数循环(Counted Loop)。

这个程序发生了什么事情呢?

  1. 启动了两个长的、不间断的循环(内部没有安全点检查)。
  2. 主线程进入睡眠状态 1 秒钟。
  3. 在1000 ms之后,JVM尝试在Safepoint停止,以便Java线程进行定期清理,但是直到可数循环完成后才能执行此操作。
  4. 主线程的 Thread.sleep 方法从 native 返回,发现安全点操作正在进行中,于是把自己挂起,直到操作结束

所以,当我们把 int 修改为 long 后,程序就表现正常了

上面的demo,在jdk10后,hotspot实现loop strip mining优化,解决了counted loop中安全点轮询问题,而且没有太多开销

long 不可数循环

@Slf4j
public class MainTest {
    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        log.info("begin:");
        Runnable runnable = () -> {
            for (long i = 0; i < 1000000000; i++) {
                num.getAndAdd(1);
            }
            log.info(Thread.currentThread().getName() + "执行结束!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        log.info("end:num = " + num);
    }
}

Output:

13:28:16.261 [main] INFO com.qhong.basic.safepoint.MainTest - begin:
13:28:17.294 [main] INFO com.qhong.basic.safepoint.MainTest - end:num = 38013556
13:29:04.171 [Thread-1] INFO com.qhong.basic.safepoint.MainTest - Thread-1执行结束!
13:29:04.174 [Thread-0] INFO com.qhong.basic.safepoint.MainTest - Thread-0执行结束!

JNI int组合优化

使用JNI进行优化:

@Slf4j
public class MainTest {
    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        log.info("begin:");
        Runnable runnable = () -> {
            for (int i = 0; i < 1000000000; i++) {
                num.getAndAdd(1);
                if (i % 1000 == 0) {
                    try {
                        Thread.sleep(0);
                    } catch (InterruptedException e) {
                        log.error("Interrupted", e);
                    }
                }
            }
            log.info(Thread.currentThread().getName() + "执行结束!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        log.info("end:num = " + num);
    }
}

Output:

13:31:33.966 [main] INFO com.qhong.basic.safepoint.MainTest - begin:
13:31:35.011 [main] INFO com.qhong.basic.safepoint.MainTest - end:num = 58127330
13:32:07.994 [Thread-1] INFO com.qhong.basic.safepoint.MainTest - Thread-1执行结束!
13:32:07.995 [Thread-0] INFO com.qhong.basic.safepoint.MainTest - Thread-0执行结束!

即使 for 循环的对象是 int 类型,也可以按照预期执行。因为我们相当于在循环体中插入了 Safepoint。

JIT(即时编译) 热点代码

上面demo中,num.getAndAdd(1)也是JNI代码,为什么没生效,没safepoint

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
    
public native int getIntVolatile(Object var1, long var2);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

这个问题跟JIT 编译器有关。由于循环体中的代码被判定为热点代码,所以经过 JIT 编译后 getAndAdd 方法的进入安全点的机会被优化掉了,所以线程不能在循环体能进入安全点。

  • 由于 num.getAndAdd 底层也是 native 方法调用,所以肯定有安全点的产生。
  • 由于虚拟机判定 num.getAndAdd 是热点代码,就来了一波优化。优化之后,把本来应该存在的安全点给干没了。

引用《深入理解JVM虚拟机》里面的描述,热点代码,主要是分为两类:

  • 被多次调用的方法。
  • 被多次执行的循环体。

禁用JIT

我可以用下面的这个参数关闭 JIT:

-Djava.compiler=NONE

@Slf4j
public class MainTest {
    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        log.info("begin:");
        Runnable runnable = () -> {
            for (int i = 0; i < 100000000; i++) {
                num.getAndAdd(1);
            }
            log.info(Thread.currentThread().getName() + "执行结束!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        log.info("end:num = " + num);
    }
}

Output:

/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/bin/java -Djava.compiler=NONE ......

14:51:58.367 [main] INFO com.qhong.basic.safepoint.MainTest - begin:
14:51:59.413 [main] INFO com.qhong.basic.safepoint.MainTest - end:num = 5204368
14:57:54.628 [Thread-1] INFO com.qhong.basic.safepoint.MainTest - Thread-1执行结束!
14:57:54.679 [Thread-0] INFO com.qhong.basic.safepoint.MainTest - Thread-0执行结束!

这里可以看出,禁用JNI循环遍历性能太差。

强制解释模式

-Xint

可以使用 -Xint 启动参数,强制虚拟机运行于“解释模式”:

@Slf4j
public class MainTest {
    public static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        log.info("begin:");
        Runnable runnable = () -> {
            for (int i = 0; i < 100000000; i++) {
                num.getAndAdd(1);
            }
            log.info(Thread.currentThread().getName() + "执行结束!");
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        log.info("end:num = " + num);
    }
}

Output:

/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/bin/java -Xint ........

15:02:37.235 [main] INFO com.qhong.basic.safepoint.MainTest - begin:
15:02:38.291 [main] INFO com.qhong.basic.safepoint.MainTest - end:num = 5093789
15:03:13.612 [Thread-0] INFO com.qhong.basic.safepoint.MainTest - Thread-0执行结束!
15:03:13.631 [Thread-1] INFO com.qhong.basic.safepoint.MainTest - Thread-1执行结束!

查看安全点轮询

如果有人想看到安全点轮询,那么可以加上这个启动参数:

-XX:+PrintAssembly

然后在输出里面找下面的关键词:

  • 如果是 OpenJDK,就找 {poll} 或 {poll return} ,这就是对应的安全点指令。
  • 如果是 Zing,就找 tls.pls_self_suspend 指令

Output:

/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/bin/java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly .......

Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
Could not load hsdis-amd64.dylib; library not loadable; PrintAssembly is disabled
15:06:13.087 [main] INFO com.qhong.basic.safepoint.MainTest - begin:
15:06:15.650 [Thread-1] INFO com.qhong.basic.safepoint.MainTest - Thread-1执行结束!
15:06:15.650 [main] INFO com.qhong.basic.safepoint.MainTest - end:num = 200000000
15:06:15.650 [Thread-0] INFO com.qhong.basic.safepoint.MainTest - Thread-0执行结束!

我这里暂时没打开,需要安装支持库。

GuaranteedSafepointInterval

把时间改的短了一点,比如 500ms,700ms 之类的,发现程序正常结束了?

为什么?

因为轮询的时间由 -XX:GuaranteedSafepointInterval 选项控制,该选项默认为 1000ms:

当你的睡眠时间比 1000ms 小太多的时候,安全点的轮询还没开始,你就 sleep 结束了,当然观察不到主线程等待的现象了

用法及建议

  • GuaranteedSafepointInterval必须配合参数-XX:+UnlockDiagnosticVMOptions使用,并且只能加在其后才能生效
  • 使用该参数的正确姿势是-XX:GuaranteedSafepointInterval=___

默认值

平台/版本 JDK6 JDK7 JDK8
Linux 1000 1000 1000
MacOS 1000 1000 1000
Windows 1000 1000 1000

参考:

没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

对线程调度中Thread.sleep(0)的深入理解

写个续集,填坑来了!关于“Thread.sleep(0)这一行‘看似无用’的代码”里面留下的坑。

posted @ 2022-12-20 15:37  hongdada  阅读(1028)  评论(0编辑  收藏  举报