本文为作者原创,转载请注明出处。
我们都知道 Java 是跨平台的,一次编译,到处运行,本质上依赖于不同操作系统下有不同的 JVM。到处运行是做到了,但运行结果呢?一样的程序,在不同的 JVM 上跑的结果是否一样呢?很遗憾,程序的执行结果没有百分百的确定性,本篇分享我遇到的一些 case。
坑一 慎用 Class.getMethods() 方法
在 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 类
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
公众号简介:作者是蚂蚁金服的一线开发,分享自己的成长和思考之路。内容涉及数据、工程、算法。