如何提高ThreadLocal包装的SimpleDateFormat的性能?

这是在RHEL上的Java 7(51)上有24个内核我们注意到,当我们增加线程池大小时,包含在本地线程中的java SimpleDateFormat的平均响应时间会增加。 这是预期的吗? 或者,我只是在做一些愚蠢的事情?

在此处输入图像描述

测试程序

public class DateFormatterLoadTest { private static final Logger LOG = Logger.getLogger(DateFormatterLoadTest .class); private final static int CONCURRENCY = 10; public static void main(String[] args) throws Exception { final AtomicLong total = new AtomicLong(0); ExecutorService es = Executors.newFixedThreadPool(CONCURRENCY); final CountDownLatch cdl = new CountDownLatch(CONCURRENCY); for (int i = 0; i < CONCURRENCY; i++) { es.execute(new Runnable() { @Override public void run() { try { int size = 65000; Date d = new Date(); long time = System.currentTimeMillis(); for (int i = 0; i < size; i++) { String sd = ISODateFormatter.convertDateToString(d); assert (sd != null); } total.addAndGet((System.currentTimeMillis() - time)); } catch (Throwable t) { t.printStackTrace(); } finally { cdl.countDown(); } } }); } cdl.await(); es.shutdown(); LOG.info("TOTAL TIME:" + total.get()); LOG.info("AVERAGE TIME:" + (total.get() / CONCURRENCY)); } } 

DateFormatter类:

 public class ISODateFormatter { private static final Logger LOG = Logger.getLogger(ISODateFormatter.class); private static ThreadLocal dfWithTZ = new ThreadLocal() { @Override public DateFormat get() { return super.get(); } @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ENGLISH); } @Override public void remove() { super.remove(); } @Override public void set(DateFormat value) { super.set(value); } }; public static String convertDateToString(Date date) { if (date == null) { return null; } try { return dfWithTZ.get().format(date); } catch (Exception e) { LOG.error("!!! Error parsing dateString: " + date, e); return null; } } } 

有人建议取出AtomicLong,所以只是想分享它在增加平均时间方面没有任何作用:

 ##NOT USING ATOMIC LONG## 2014-02-28 11:03:52,790 [pool-1-thread-1] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:328 2014-02-28 11:03:52,868 [pool-1-thread-6] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:406 2014-02-28 11:03:52,821 [pool-1-thread-2] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:359 2014-02-28 11:03:52,821 [pool-1-thread-8] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:359 2014-02-28 11:03:52,868 [pool-1-thread-4] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:406 2014-02-28 11:03:52,915 [pool-1-thread-5] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:453 2014-02-28 11:03:52,930 [pool-1-thread-7] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:468 2014-02-28 11:03:52,930 [pool-1-thread-3] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:468 2014-02-28 11:03:52,930 [main] INFO net.ahm.graph.DateFormatterLoadTest - CONCURRENCY:8 ##USING ATOMIC LONG## 2014-02-28 11:02:53,852 [main] INFO net.ahm.graph.DateFormatterLoadTest - TOTAL TIME:2726 2014-02-28 11:02:53,852 [main] INFO net.ahm.graph.DateFormatterLoadTest - CONCURRENCY:8 2014-02-28 11:02:53,852 [main] INFO net.ahm.graph.DateFormatterLoadTest - AVERAGE TIME:340 ##NOT USING ATOMIC LONG## 2014-02-28 11:06:57,980 [pool-1-thread-3] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:312 2014-02-28 11:06:58,339 [pool-1-thread-8] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:671 2014-02-28 11:06:58,339 [pool-1-thread-4] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:671 2014-02-28 11:06:58,307 [pool-1-thread-7] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:639 2014-02-28 11:06:58,261 [pool-1-thread-6] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:593 2014-02-28 11:06:58,105 [pool-1-thread-15] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:437 2014-02-28 11:06:58,089 [pool-1-thread-13] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:421 2014-02-28 11:06:58,073 [pool-1-thread-1] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:405 2014-02-28 11:06:58,073 [pool-1-thread-12] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:405 2014-02-28 11:06:58,042 [pool-1-thread-14] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:374 2014-02-28 11:06:57,995 [pool-1-thread-2] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:327 2014-02-28 11:06:57,995 [pool-1-thread-16] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:327 2014-02-28 11:06:58,385 [pool-1-thread-10] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:717 2014-02-28 11:06:58,385 [pool-1-thread-11] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:717 2014-02-28 11:06:58,417 [pool-1-thread-9] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:749 2014-02-28 11:06:58,418 [pool-1-thread-5] INFO net.ahm.graph.DateFormatterLoadTest - THREAD TIME:750 2014-02-28 11:06:58,418 [main] INFO net.ahm.graph.DateFormatterLoadTest - CONCURRENCY:16 ##USING ATOMIC LONG## 2014-02-28 11:07:57,510 [main] INFO net.ahm.graph.DateFormatterLoadTest - TOTAL TIME:9365 2014-02-28 11:07:57,510 [main] INFO net.ahm.graph.DateFormatterLoadTest - CONCURRENCY:16 2014-02-28 11:07:57,510 [main] INFO net.ahm.graph.DateFormatterLoadTest - AVERAGE TIME:585 

SimpleDateFormat不是线程安全的

正如Martin Wilson所说的正确答案 ,实例化SimpleDateFormat相对昂贵。

知道你的第一个想法可能是,“好吧,让我们缓存一个实例以便重复使用。” 很好的想法,但要注意:SimpleDateFormat类不是线程安全的 。 所以在同步标题下说类文档 。

乔达时间

一个更好的解决方案是避免臭名昭着(现在已经过时)的java.util.Date,.Calendar和SimpleDateFormat类。 而是使用:

  • 乔达时间
    第三方开源库,日期/日历的流行替代品。
  • java.time包
    新的,捆绑在Java 8中 ,取代旧的Date / Calendar类,受JSR 310定义的Joda-Time的启发。

Joda-Time有意构建为线程安全的,主要是通过使用不可变对象 。 有一些可变类,但通常不使用。

StackOverflow上的另一个问题解释了DateTimeFormatter类确实是线程安全的。 因此,您可以创建一个实例,对其进行缓存,并让所有线程使用该格式化程序,而无需添加任何额外的同步或其他并发控制。

另一种加速格式化的方法是缓存格式化结果。 这考虑了这样一个事实,即格式通常没有那么多不同的日期。 如果您拆分日期和时间的格式,它甚至是更好的缓存候选者。

这样做的缺点是,正常的Java缓存实现(如EHCache)要慢,缓存访问只需要比格式化更长的时间。

还有另一个缓存实现,其访问时间与HashMap相同。 在这种情况下,你获得了很好的加速。 在这里,您可以找到我的概念validation测试: https : //github.com/headissue/cache2k-benchmark/blob/master/zoo/src/test/java/org/cache2k/benchmark/DateFormattingBenchmark.java

也许这可以成为您方案中的解决方案。

免责声明:我正在使用cache2k ….

创建SimpleDateFormat的实例非常昂贵( 本文展示了一些分析/基准测试)。 如果这是真的,与将日期解析为字符串相比,那么随着您增加线程数(因此增加SimpleDateFormat实例的数量,因为它们是threadlocals),您的平均时间将会增加。

我们的用例是一次写入(单线程)并多次读取(并发)。 所以我在存储数据时将Date转换为String,而不是每次需要响应请求时都这样做。