资讯专栏INFORMATION COLUMN

Java跨平台?慎用这些有平台差异性的方法

hidogs / 1174人阅读

摘要:坑一慎用方法在类中,有一个方法是,返回的是一个数组,该数组包含了所包含的方法。坑二慎用线程优先级做并发处理线程中有属性,表示线程的优先级,默认值为,取值区间为。显然,运行时环境是因操作系统而异的。

本文为作者原创,转载请注明出处。

我们都知道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

公众号简介:作者是蚂蚁金服的一线开发,分享自己的成长和思考之路。内容涉及数据、工程、算法。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76221.html

相关文章

  • Effective Java 第三版 全文翻译

    摘要:本章中的大部分内容适用于构造函数和方法。第项其他方法优先于序列化第项谨慎地实现接口第项考虑使用自定义的序列化形式第项保护性地编写方法第项对于实例控制,枚举类型优先于第项考虑用序列化代理代替序列化实例附录与第版中项目的对应关系参考文献 effective-java-third-edition 介绍 Effective Java 第三版全文翻译,纯属个人业余翻译,不合理的地方,望指正,感激...

    galois 评论0 收藏0
  • Docker大坑小洼

    摘要:正在学习,留着看看转自的大坑小洼成为云计算领域的新宠儿已经是不争的事实,作为高速发展的开源项目,难免存在这样或那样的瑕疵。话不多说,一起来领略的大坑小洼。原因回归至上文的第一个坑。如此一来,只要内部涉及到域名解析,则立即受到影响。 正在学习Docker,留着看看 转自Docker的大坑小洼 Docker成为云计算领域的新宠儿已经是不争的事实,作为高速发展的开源项目,难免存在这样或那样...

    My_Oh_My 评论0 收藏0
  • Hyperledger Fabric(介绍)

    摘要:比特币和以太币属于一类区块链,我们将其归类为公共无许可的区块链技术。例如,在单个企业中部署时,或由受信任的权威机构运作,完全拜占庭容错的共识可能被认为是不必要的,并且对性能和吞吐量造成过度的拖累。 介绍 一般而言,区块链是一个不可变的交易分类账,维护在一个分布式对等节点网络中。这些节点通过应用已经由共识协议验证的交易来维护分类帐的副本,该交易被分组为包括将每个块绑定到前一个块的散列的块...

    yunhao 评论0 收藏0
  • 初次接触java

    摘要:运行环境解释器开发工具包编译器类库工具安装执行安装包环境变量配置安装目录,让第三方依赖于的软件使用的工具命令所在目录,已有值后拼接字节码文件所在目录,一般配置当前目录第一个程序格式类名如编译源文件名运行类名中的代码都是包含在类之中。 计算机组成: 输出设备 输入设备 运算器、控制器(cpu) 存储器(硬盘、内存) --冯洛伊曼体系结构 计算机中数据处理方式:二进制、只有加法 原码:二...

    maxmin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<