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敏感性程序来讲,可以通过类似缓存的策略来周期性获取该值
  • 工作中的性能问题可能并不是该函数导致,可能是其他问题导致

感谢

猜你喜欢

转载自juejin.im/post/5d7e3f93f265da03c23f04ce