Thread.sleep(0)有用吗?
了解这个问题首先需要知道一个知识点:safepoint,安全点。
关于安全点的描述,我们可以看看《深入理解 JVM 虚拟机(第三版)》的 3.4.2 小节:

其中重点描述:
有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。
在程序进入 Safepoint 的时候, Java 线程可能正处于五种不同的状态,分别是:

主要聚焦于和本文相关的第二点:Running in native code。
When returning from the native code, a Java thread must check the safepoint _state to see if we must block.
第一句话,就是答案,意思就是一个线程在运行 native 方法后,返回到 Java 线程后,必须进行一次 safepoint 的检测。而sleep 方法就是一个 native 方法。
所以,到这里我们可以确定的是:调用 sleep 方法的线程会进入 Safepoint。
实践:
public class MainTest {
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);
}
}
这个代码,你直接粘到你的 IDEA 里面去就能跑。按照代码来看,主线程休眠 1000ms 后就会输出结果,但是实际情况却是主线程一直在等待 t1,t2 执行结束才继续执行。
Connected to the target VM, address: '127.0.0.1:52972', transport: 'socket'
Thread-1执行结束!
num = 2000000000
Thread-0执行结束!
Disconnected from the target VM, address: '127.0.0.1:52972', transport: 'socket'
Process finished with exit code 0
这个程序发生了什么事情呢?
- 1.启动了两个长的、不间断的循环(内部没有安全点检查)。
- 2.主线程进入睡眠状态 1 秒钟。
- 3.在 1000 ms 之后,JVM 尝试在 Safepoint 停止,以便 Java 线程进行定期清理,但是直到可数循环完成后才能执行此操作。
- 4.主线程的 Thread.sleep 方法从 native 返回,发现安全点操作正在进行中,于是把自己挂起,直到操作结束。
设置安全点在测试一下:
public class MainTest { 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); //prevent gc 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 = " + num); } }
我们看一下运行的结果:
Connected to the target VM, address: '127.0.0.1:53053', transport: 'socket'
num = 72548852
Thread-1执行结束!
Thread-0执行结束!
Disconnected from the target VM, address: '127.0.0.1:53053', transport: 'socket'
Process finished with exit code 0
因为我们相当于在循环体中插入了 Safepoint。在 1000 ms 之后,JVM 尝试在 Safepoint 停止,就可以停止下来。
优化:
把 int 修改为 long,然后就可以直接把 for 循环里面的 if 逻辑删除掉了。
继续看《深入理解 JVM 虚拟机(第三版)》,书的 5.2.8 小节:由安全点导致长时间停顿。
里面有这样一段话:

我们仔细读一下划线部分:
是 HotSpot 虚拟机为了避免安全点过多带来过重的负担,对循环还有一项优化措施,认为循环次数较少的话,执行时间应该也不会太长,所以使用 int 类型或范围更小的数据
类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环(Counted Loop),相对应地,使用 long 或者范围更大的数据类型作为索引值的循环就被称为
不可数循环(Uncounted Loop),将会被放置安全点。
意思就是在可数循环(Counted Loop)的情况下,HotSpot 虚拟机搞了一个优化,就是等循环结束之后,线程才会进入安全点。
反过来说就是:循环如果没有结束,线程不会进入安全点,GC 线程就得等着当前的线程循环结束,进入安全点,才能开始工作。
所以,修改方案就是把 int 修改为 long。
原理就是让其变为不可数循环(Uncounted Loop),从而不用等循环结束,在循环期间就能进入 Safepoint。
再次编辑测试一下:
public class MainTest { public static AtomicInteger num = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Runnable runnable=()->{ for (long 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); } }
同样是可以得到预期的结果:
Connected to the target VM, address: '127.0.0.1:53518', transport: 'socket'
num = 81672596
Thread-0执行结束!
Thread-1执行结束!
Disconnected from the target VM, address: '127.0.0.1:53518', transport: 'socket'
Process finished with exit code 0

浙公网安备 33010602011771号