典型编程案例分析(一)— 谈谈你最“熟悉”的JDK

举报
牛哥 发表于 2020/06/15 09:28:49 2020/06/15
【摘要】 前言JDK是一个强大的开发工具集,里面提供了大量的通用类和API,在Java代码开发过程中也被大量使用,但事实上,你对JDK真有你想象当中的那么“熟悉”吗?在代码检视过程中,经常发现的一些JDK中的类或API未被合理地、正确地使用,本篇将列举几种最常用的案例,并进行分析说明。案例分析1. 被误用的SimpleDateFormatSimpleDateFormat在平常的开发过程中比...

前言

JDK是一个强大的开发工具集,里面提供了大量的通用类和API,在Java代码开发过程中也被大量使用,但事实上,你对JDK真有你想象当中的那么“熟悉”吗?

在代码检视过程中,经常发现的一些JDK中的类或API未被合理地、正确地使用,本篇将列举几种最常用的案例,并进行分析说明。

案例分析

1.       被误用的SimpleDateFormat

SimpleDateFormat在平常的开发过程中比较常用,我简单的搜索了一下,发现我们团队现在的产品中有将近200处使了该类,但是在平常的代码检视过程中,发现被误用的情况还是比较多的。

SimpleDateFormat是线程安全对象,但很多人并不知道,而是想当然将它当成线程安全的工具类使用,直接应用于多线程并发场景,比如下面这段代码。

  错误的用法 

public class AlarmUtil {
  ...
  private static SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  ...
  public static void updateAlarm(){
    ...
        aMsg.setAlarmGenTime(df.parse(df.format(new Date())));
    ...
  }
  ...
}

当上面代码片段中的updateAlarm方法被高频率并发调用的时候,极有可能出现结果不符合预期,或者抛异常的情况。

正确的用法应当是避免SimpleDateFormat对象被并发访问。

  正确的用法

public class AlarmUtil {
  ...
  public static void updateAlarm(){
    ...
    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      aMsg.setAlarmGenTime(df.parse(df.format(new Date())));
    ...
  }
  ...
}

2.       被错用的System.currentTimeMillis()

System.currentTimeMillis()的作用是获取当前系统时间戳,精度为毫秒,这点相信大家并不陌生。在代码检视过程,发现该方法常在计时场景中,用于开始时间和结束时间的获取。事实上这种用法是有问题的,原因是此方法取的是系统当前时间,请注意,这个时间在Java程序运行过程中,是可以人为修改的,因此在计时的场景中使用该方法是不可靠的。比如在下面这段代码中,在获取了起始时间之后,到获取结束时间之前的这段时间里,如果有人将系统时间往过去调了,则计算出来的时间差可能比实际时间差要短,甚至可能是个负数,反之,如果往未来调了,则计算出来的时间差可能比实际时间差要长。

  错误的用法

long start = System.currentTimeMillis();
...
long end = System.currentTimeMillis();
...
logger.info("       ===== The thread consumes time is : " + (end - start) + " ms");

在计时场景中,获取开始时间和结束时间正确的做法,应当是使用System.nanoTime()System.nanoTime()专用于测量已过的时间,与系统或钟表时间等其他任何时间概念无关,程序运行过程中,外界是无法干预的。

  正确的用法

long start = System.nanoTime();
...
long end = System.nanoTime();
...
long usedTime = TimeUnit.MILLISECONDS.convert((end - start), TimeUnit.NANOSECONDS);
logger.info("       ===== The thread consumes time is : " + usedTime + " ms");

3.       被滥用的StringBuilder

在编程军规中有一条“在进行三个字符串(不包含三个)以上的串联操作时必须使用StringBuilderStringBuffer,禁止使用‘+”,原因是StringBuilderStringBuffer要比“+”操作符的性能高。于是大家便认为,在进行字符串连接的时候,使用StringBuilder一定是没错的,便开始在自己的代码中大量使用。

然而,是不是所有字符串连接的场景都有必要使用StringBuilder呢?答案当然是否定的。比如在下面这段代码,其实使用StringBuilder和直接使用“+”操作符,在性能上并无差异。研究过Java编译及.class文件结构的人一定知道,Java代码中的“+”表达式最终都会被编译成new StringBuilder(xxx)….append(xx).toString()的形式。因此在连接固定个数的字符串时,使用StringBuilder并不能带来明显的性能提升,反而会降低代码的可读性。

  滥用的情况



public static String errorMsg(String content) {
  return new StringBuilder(">>>>>>Error is: ").append(content).append(">>>>>>").toString();
}

但是,话又说回来,当需要连接的字符串个数不固定的时候,比如要将一个列表中的所有字符串进行连接,此时是一定要使用StringBuilder的,连接字符串个数越多,带来的性能提升就越明显。

4.       被遗忘的ThreadLocal

在案例1中,我们是要求将SimpleDateFormat挪到方法内部去实例化,这样就避免了并发访问的问题。有些同事也许就会问了,这样每次调用方法的时候,都要new一个SimpleDateFormat对象,会不会有性能问题?这个问题很好,这样做确实会带来一定的性能损失,可是不这样做,我们又会存在并发问题。愁死人了,难道就没有两全其美的方法?即解决并发的问题,又不至于让性能损失太多。

这个时候我们就需要用于ThreadLocal类了。ThreadLocal就好比是一个Map,它以线程对象作为Key,每个线程调用它的get方法,获取的都是专属于这个线程的Value。在内存富余的情况下,这个专属对象会一直放在ThreadLocal对象中,下次再get的时候,获取的还是同一个对象,这样便解决了频繁创建的问题。

细心的同事会发现,我前面特别提到是在“内存富余”的情况下,如果内存不富余呢?ThreadLocal中是通过软引用持有Value对象的,当内存比较紧张的时候,ThreadLocal中的未被使用的Value有可能会垃圾回收掉。因此,通常我们在使用ThreadLocal的时候,会复写它的initialValue方法,保证Value被回收掉之后,下次再调get方法,可以获得一个新的Value,而不是null

按照这个方法,将案例1中的代码优化一下。

  优化的代码

public class AlarmUtil {
  ...
  static final ThreadLocal<SimpleDateFormat> FORMAT_CACHE = new ThreadLocal<SimpleDateFormat>() {
    @Override
    protected SimpleDateFormat initialValue() {
      return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
  };
  ...
  public static void updateAlarm(){
    ...
    SimpleDateFormat df = FORMAT_CACHE.get();
    // 如果需要,别忘了在使用之前,重置对象
    aMsg.setAlarmGenTime(df.parse(df.format(new Date())));
    ...
  }
  ...
}

大家要注意的是,由于ThreadLocal中的对象主要用来被复用的,因此在get出来之后,首先做的就是重置该对象,重置的方法可能是重新初始化,也可能是清空,也有可能是其它。

当然,ThreadLocal也不是万能的,从它的实现原理,我们可以知道,当我们的应用如果不是基于线程池的,使用ThreadLocal其实并不能给我们带来性能上的提升。

PS:在平常的交流中,发现有部分同事也有用到ThreadLocal对象,但是当做线程上下文来用,用于在前后代码逻辑传递一些对象,这实际上是不可取的,原因就在于,我们前面提到的,ThreadLocal是通过软引用持有对象的,在程序运行的过程,ThreadLocal中持有的对象极有可能被垃圾回收机制回收掉,从而导致后续代码因为获取不到这些对象而出错。

结语

  本篇只列举了4种最常见的对JDK的类和API使用不当的场景,其实类似的问题还有很多。上面的例子能给我们带来什么启示?我们在使用任何API前,一定仔细阅读其API说明,有必要时,还要了解清楚其内部实现原理,这样才能做到不错用、不误用、不滥用、不漏用。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。