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)這一行‘看似無用’的代碼”裏面留下的坑。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章