案例出自 马士兵MAC课程
案例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public class OSRDemo { static long counter;
public static void main(String[] args) throws Exception { System.out.println("main start"); startBusinessThread(); startProblemThread(); Thread.sleep(500); System.gc(); System.out.println("main end"); }
private static void startProblemThread() { new Thread(() -> { System.out.println("Problem start"); for (int i = 0; i < 1000000; i++) { for (int j = 0; j < 100000; j++) { counter += i % 33; counter += i % 333; } } }).start(); }
private static void startBusinessThread() { new Thread(() -> { System.out.println(Thread.currentThread().getName() + " start"); while (true) { System.out.println("执行业务一"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "thread-01").start(); } }
|
案例现象
- 主线程创建两个线程,其中一个线程执行业务逻辑,每秒进行一次打印;另一个线程执行循环运算
- 启动线程后,JVM 进行 GC 垃圾回收,此时可以发现业务线程和主线程均被阻塞
分析原因
- JVM 进行 GC 时,线程需要中断,因此所有线程都需要‘跑’到线程安全点(save point)后,再停顿下来。
- 在 JDK1.8 及之前版本,C2 编辑器认为 int 的循环为有限循环,因此 startProblemThread() 创建的线程将不会进入线程安全点
- 主线程和业务线程则进入线程安全点阻塞等待,而 startProblemThread() 创建的线程并不会发生中断,导致 GC 没法完成
- 因此主线程和业务线程一直阻塞
解决措施
将 JDK 版本换成 11
重新运行后发现不会阻塞
将 int 循环改为 long 值循环
因为 long 值循环意味着这是一个较大的循环,JVM 会进行线程安全点的检查
在循环中加入方法调用
线程安全点的检查发生在方法调用前
注意:如果调用的方法是空方法或者简单循环,则编译器会将其优化,不会继续线程安全点的检测,因此还是会阻塞
用 volatile 修饰 counter
volatile 修饰的代码不会进行优化
添加 JVM 参数 -XX:-UseOnStackReplacement
关闭 OnStackReplacement 优化
添加 JVM 参数 -XX:TieredStopAtLevel=3
不使用 C2 编译器的优化