本文为作者原创,转载请注明出处。
我们都知道Java是跨平台的,一次编译,到处运行,本质上依赖于不同操作系统下有不同的JVM。到处运行是做到了,但运行结果呢?一样的程序,在不同的JVM上跑的结果是否一样呢?很遗憾,程序的执行结果没有百分百的确定性,本篇分享我遇到的一些case。
在Class类中,有一个方法是getMethods(),返回的是一个Method数组,该数组包含了Class所包含的方法。但是需要注意的是,其数组元素的排序是不确定的,在不同的机器上会有不一样的排序输出。
public Method[] getMethods() throws SecurityException { checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true); return copyMethods(privateGetPublicMethods()); }
阿里的fastjson就曾经在这里踩到坑了,fastjson是序列化框架,当要去获取对象的某个属性值时,往往需要通过反射调用getter方法。比如,有个属性field,那么通过遍历Method数组,判断是否有getField方法,如果有的话,则调用取得相应的值。
但对于boolean类型的字段,其getter方法有可能是isXXX,也有可能是getXXX,而fastjson在遍历时,只要判断有isXXX或者getXXX,就认定其为getter方法,然后立即执行该getter方法。
// 伪代码 for (Method method : someObject.class.getMethods()) { // 判断是否为getter方法 if(method.getName().equals("getField") || method.getName().equals("isField")){ // 通过getter取得属性值 return method.invoke(xxx, xxxx); } }
但是如果一个对象同时存在isA和getA方法呢?
private A a; private boolan isA(){ return false; } private A getA(){ return a; }
这个时候fastjson到底执行的是isA()还是getA()呢?答案是不确定,因为isA和getA在返回的Method数组中顺序是不确定的,所以有的机器上可能是通过isA()来获取属性值,有的机器上可能是通过getA()来获取属性值,而这两个方法返回的一个是boolean类型,一个是A类型,导致fastjson在不同机器执行的结果是不一样的。
为什么这个方法返回值不按照字母排序呢?每个类或者方法名字都会对应一个Symbol对象,在这个名字第一次使用的时候构建,Symbol对象是通过malloc来分配的,因此新分配的Symbol对象的地址就不一定比后分配的Symbol对象地址小,也不一定大,因为期间存在内存free的动作,那地址是不会一直线性变化的,之所以不按照字母排序,主要还是为了速度考虑,根据Symbol对象的地址排序是最快的。
线程Thread中有priority属性,表示线程的优先级,默认值为5,取值区间为[1,10]。虽然在Thread的注释中有说明优先级高的线程将会被优先执行,但是测试结果,却是随机的。
如下,
static class Runner implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName()+"---"+i); } } } public static void main(String[] args) { Thread t1 = new Thread(new Runner(), "thread-1"); Thread t2 = new Thread(new Runner(), "thread-2"); Thread t3 = new Thread(new Runner(), "thread-3"); t1.setPriority(10); // t1 线程优先级设置为10 t2.setPriority(5); // t2 线程优先级设置为5 t3.setPriority(1); // t3 线程优先级设置为1 t1.start(); t2.start(); t3.start(); }
如果是严格按照线程优先级来执行的,那么应该是t1执行for循环,然后t2执行完for循环,最后t3执行for循环。但实际上测试结果显示,每次执行的输出顺序都没有遵循这个规则,并且每次执行的结果都是不一样的。
---- console output ---- thread-2---0 thread-2---1 thread-3---0 thread-1---0 thread-1---1 thread-1---2 thread-3---1 ...... ......
线程调度具有很多不确定性,线程的优先级只是对线程的一个标志,但不代表着这是绝对的优先,具体的执行顺序都是由操作系统本身的资源调度来决定的。不同操作系统本身的线程调度方式可能存在差异性,所以不能依靠线程优先级来处理并发逻辑。
Java API中,一般使用native方法System.currentTimeMillis() 来获取系统的时间。从方法名上,可以看出,该方法用于获取系统当前的时间,即从1970年1月1日8时到当前的毫秒值。
下面罗列出了官方对该方法的注释:
public final class System { /** * Note that while the unit of time of the return value is a millisecond, * the granularity of the value depends on the underlying * operating system and may be larger. For example, many * operating systems measure time in units of tens of * milliseconds. */ public static native long currentTimeMillis(); }
方法注释明确指出了这个毫秒值的精度在不同的操作系统中是存在差异的,有的系统1毫秒实际上等同于物理时间的几十毫秒。也就是说,在一个性能测试中,因为精度不一致的问题,有的系统得出的结果是1毫秒,另外系统得出的性能结果却是10毫秒。
那如何实现高精度的时间计算呢?先来看看System.nanoTime()方法,下面列出了官方的核心注释:
public final class System { /** * This method can only be used to measure elapsed time and is * not related to any other notion of system or wall-clock time. */ public static native long nanoTime(); }
这个方法只能用于检测系统经过的时间,也就是说其返回的时间不是从1970年1月1日8时开始的纳秒时间,是从系统启动开始时开始计算的时间。
所以一般高精度的时间是采用System.nanoTime()方法来实现的,其单位为纳秒(十亿分之一秒),虽然不保证完全准确的纳秒级精度。但用该方法来实现毫秒级精度的计算,是绰绰有余的,如下。
long start = System.nanoTime(); // do something long end = System.nanoTime(); // 程序执行的时间,精确到毫秒 long costTime = (end - start) / 1000000L
Runtime是JVM中运行时环境的抽象,包含了运行时环境的一些信息,每个Java应用程序都有一个Runtime实例,用于应用程序和其所在的运行时环境进行交互。应用程序本身无法创建Runtime实例,只能通过Runtime.getRuntime()方法来获取。
显然,运行时环境是因操作系统而异的。其交互方式也存在差异,
例如,
// Windows下调用程序 Process proc =Runtime.getRuntime().exec("exefile"); // Linux下调用程序 Process proc =Runtime.getRuntime().exec("./exefile");
所以,如果应用程序中包含这类和运行时环境进行交互的方法,应确保应用的部署环境不变,如果不能保证的话,那么至少需要提供两套运行时交互逻辑。
以上是我遇到的不能跨平台的一些case,其实本质上都和native实现有关。你有没有遇到一些这样的坑呢?欢迎留言~
参考链接:
JVM源码分析之不保证顺序的Class.getMethods
公众号简介:作者是蚂蚁金服的一线开发,分享自己的成长和思考之路。内容涉及数据、工程、算法。