代码中获取时区是一个很平常的操作,例如我们要获取东八区,可以TimeZone.getTimeZone("GMT+08")这样写,也可以写成TimeZone.getTimeZone("GMT+0800"),毫无疑问,这两种写法的结果是一样的。那是不是就说明这两种写法是等价的,进而我们在使用它们时可以随便一会儿用写法一,一会儿用写法二,二者之间随意切换呢?请看下面测试代码,只执行100000次的TimeZone.getTimeZone("GMT+0800")情况下,耗时为34ms。
long start = System.currentTimeMillis(); for(int i=0;i<100000;i++) { TimeZone.getTimeZone("GMT+0800"); } long end = System.currentTimeMillis(); System.out.println("耗时:"+(end - start));
可是当我们在循环执行TimeZone.getTimeZone("GMT+0800")之前,执行一次TimeZone.getTimeZone("GMT+08"),耗时立刻变为2107ms!
TimeZone.getTimeZone("GMT+08"); long start = System.currentTimeMillis(); for(int i=0;i<100000;i++) { TimeZone.getTimeZone("GMT+0800"); } long end = System.currentTimeMillis(); System.out.println("耗时:"+(end - start));
将GMT+08和GMT+0800的位置互换也会看到相同的现象,也就是说TimeZone.getTimeZone("GMT+0800")和TimeZone.getTimeZone("GMT+08")两种写法虽然得到的结果一样,但两者同时使用时,会相互影响,导致方法耗时增加明显。可千万别小看这种差距,反映到真正的业务应用中可能就是几百几千TPS的差距!
通过结合源码分析,最终将问题简化如下:
public static void main(String[] args) { simulateGetTimeZone("GMT+08"); long start = System.currentTimeMillis(); for(int i=0;i<100000;i++) { simulateGetTimeZone("GMT+0800"); } long end = System.currentTimeMillis(); System.out.println("耗时:"+(end - start)); } public static void simulateGetTimeZone(String id) { ZoneInfoFile.getZoneInfo(id); ZoneInfoFile.getCustomTimeZone(id, 28800000); }
调用simulateGetTimeZone方法产生的现象和调用TimeZone.getTimeZone时是类似的,因此我们此时只要分析为什么 ZoneInfoFile.getZoneInfo和ZoneInfoFile.getCustomTimeZone两个方法同时执行时就会产生问题描述中的奇怪现象。两个方法的源码如下,为了便于后面分析,在部分代码后添加了标号:
public static ZoneInfo getZoneInfo(String id) { ZoneInfo zi = getFromCache(id);//1 if (zi == null) { zi = createZoneInfo(id);//2 if (zi == null) { return null; } zi = addToCache(id, zi); } return (ZoneInfo) zi.clone(); }
public static ZoneInfo getCustomTimeZone(String originalId, int gmtOffset) { String id = toCustomID(gmtOffset);//3 ZoneInfo zi = getFromCache(id);//4 if (zi == null) { zi = new ZoneInfo(id, gmtOffset); zi = addToCache(id, zi);//5 if (!id.equals(originalId)) { zi = addToCache(originalId, zi);//6 } } return (ZoneInfo) zi.clone(); }
当先执行simulateGetTimeZone("GMT+08");一次时,第三步得到的id为GMT+08:00,然后分别在第五步和第六步添加id为GMT+08:00和GMT+08的ZoneInfo到缓存中,后续如果继续执行simulateGetTimeZone("GMT+08"),那么在第一步就会返回结果;可是如果后续执行的是simulateGetTimeZone("GMT+0800"),由于没有id为GMT+0800的缓存,所以继续执行到第三步,得到id仍为GMT+08:00,这个id是有缓存的,因此在第四步就可以返回,从而不会添加id为GMT+0800的ZoneInfo到缓存中,也就是说以后每次都要执行到第四步才可以返回结果,而不是执行到第一步就可以直接返回结果。先执行一次simulateGetTimeZone("GMT+08"),会让后面simulateGetTimeZone("GMT+0800")的每一次执行都多执行第一步和第四步之间的步骤,从而方法的耗时也会明显增加!
获取时区虽是一个简单的操作,但如果不注意里面的细节,对应用性能的影响将会是巨大的。对这种常量数据应该定义为静态的,且要一处定义,N处使用;切不可N处定义,N处使用,这样说不定哪天就掉到文章一开始描述的那个坑里面了……