Runtime.availableProcessors() 分析
最近看到一篇文章Docker面對Java將不再尷尬:Java 10為Docker做了特殊優化,裡面提到了java10對於docker做了一些特殊的優化。眾所周知java的docker容器化支援一直以來都比較的尷尬,由於docker底層使用了cgroups來進行程式級別的隔離,雖然我們通過docker設定了容器的資源限制,但jvm虛擬機器器其實感知不到這裡些限制。比如我們的宿主機可能是8核16G,限定docker容器為2核4G,在容器中讀出來的資源可能還是8核16G,我們平時可能會來讀取機器資源來做效能優化,比如核心執行緒數、最大執行緒數的設定。這對於一些程式來講,在docker上跑可能會會帶來效能損耗,所幸的是java10已經增加了這些支援,並且有jdk8相容的計劃。
想起最近工作中,在優化程式過程中發現availableProcessors
似乎有較大效能損耗,因此對它進行了詳細的瞭解並做了一些測試。
availableProcessors 提供了什麼功能?
/**
* Returns the number of processors available to the Java virtual machine.
*
* <p> This value may change during a particular invocation of the virtual
* machine. Applications that are sensitive to the number of available
* processors should therefore occasionally poll this property and adjust
* their resource usage appropriately. </p>
*
* @return the maximum number of processors available to the virtual
* machine; never smaller than one
* @since 1.4
*/
public native int availableProcessors();
複製程式碼
jdk檔案中這麼寫到,返回jvm虛擬機器器可用核心數。並且後面還有一段註釋:這個值有可能在虛擬機器器的特定呼叫期間更改。我們平時對於此函式的直觀印象為:返回機器的CPU數,這個應該是一個常量值。由此看來,可能有很大的一些誤解。由此我產生了兩個疑問:
- 1、何為JVM可用核心數?
- 2、為何返回值可變?它是如何工作的?
JVM可用核心數
這個比較好理解,顧名思義為JVM可以用來工作利用的CPU核心數。在一個多核CPU伺服器上,可能安裝了多個應用,JVM只是其中的一個部分,有些cpu被其他應用使用了。
為何返回值可變?它是如何工作的?
返回值可變這個也比較好理解,既然多核CPU伺服器上多個應用公用cpu,對於不同時刻來講可以被JVM利用的數量當然是不同的,既然如此,那java中是如何做的呢? 通過閱讀jdk8的原始碼,linux系統與windows系統的實現差別還比較大。
linux 實現
int os::active_processor_count() {
// Linux doesn't yet have a (official) notion of processor sets,// so just return the number of online processors.
int online_cpus = ::sysconf(_SC_NPROCESSORS_ONLN);
assert(online_cpus > 0 && online_cpus <= processor_count(),"sanity check");
return online_cpus;
}
複製程式碼
linux 實現比較懶,直接通過sysconf讀取系統引數,_SC_NPROCESSORS_ONLN。
windows 實現
int os::active_processor_count() {
DWORD_PTR lpProcessAffinityMask = 0;
DWORD_PTR lpSystemAffinityMask = 0;
int proc_count = processor_count();
if (proc_count <= sizeof(UINT_PTR) * BitsPerByte &&
GetProcessAffinityMask(GetCurrentProcess(),&lpProcessAffinityMask,&lpSystemAffinityMask)) {
// Nof active processors is number of bits in process affinity mask
int bitcount = 0;
while (lpProcessAffinityMask != 0) {
lpProcessAffinityMask = lpProcessAffinityMask & (lpProcessAffinityMask-1);
bitcount++;
}
return bitcount;
} else {
return proc_count;
}
}
複製程式碼
windows系統實現就比較複雜,可以看到不僅需要判斷CPU是否可用,還需要依據CPU親和性去判斷是否該執行緒可用該CPU。裡面通過一個while迴圈去解析CPU親和性掩碼,因此這是一個CPU密集型的操作。
效能測試
通過如上分析,我們基本可以知道這個操作是一個cpu敏感型操作,那麼它的效能在各個作業系統下表現如何呢?如下我測試了該函式在正常工作何cpu滿負荷工作情況下的一些表現。測試資料為執行100萬次呼叫,統計10次執行情況,取平均值。相關程式碼如下:
public class RuntimeDemo {
private static final int EXEC_TIMES = 100_0000;
private static final int TEST_TIME = 10;
public static void main(String[] args) throws Exception{
int[] arr = new int[TEST_TIME];
for(int i = 0; i < TEST_TIME; i++){
long start = System.currentTimeMillis();
for(int j = 0; j < EXEC_TIMES; j++){
Runtime.getRuntime().availableProcessors();
}
long end = System.currentTimeMillis();
arr[i] = (int)(end-start);
}
double avg = Arrays.stream(arr).average().orElse(0);
System.out.println("avg spend time:" + avg + "ms");
}
}
複製程式碼
CPU 滿負荷程式碼如下:
public class CpuIntesive {
private static final int THREAD_COUNT = 16;
public static void main(String[] args) {
for(int i = 0; i < THREAD_COUNT; i++){
new Thread(()->{
long count = 1000_0000_0000L;
long index=0;
long sum = 0;
while(index < count){
sum = sum + index;
index++;
}
}).start();
}
}
}
複製程式碼
系統 | 配置 | 測試方法 | 測試結果 |
---|---|---|---|
Windows | 2核8G | 正常 | 1425.2ms |
Windows | 2核8G | CPU 滿負荷 | 6113.1ms |
MacOS | 4核8G | 正常 | 69.4ms |
MacOS | 4核8G | CPU滿負荷 | 322.8ms |
雖然兩個機器的配置相差較大,測試資料比較意義不大,但從測試情況還是可以得出如下結論:
- windows與類linux系統效能差異較大,與具體實現有關
- CPU密集型計算對於該函式效能有較大的影響
- 整體上講,該函式效能還是比較可以接受的,最長的那次為windows CPU滿負荷下 也僅為6us。linux系統下可以降到ns級別。
總結
- 日常工作中,並不太需要注意該函式的呼叫效能負荷
- 如需使用一般定義成靜態變數即可,對於cpu敏感性程式來講,可以通過類似快取的策略來週期性獲取該值
- 工作中的效能問題可能並不是該函式導致,可能是其他問題導致