Подтвердить что ты не робот

Как повысить производительность SimpleDateFormat, завернутого в ThreadLocal?

Это на Java 7 (51) на RHEL с 24 ядрами Мы отмечаем увеличение средних времен откликов java SimpleDateFormat, завернутых в поток, когда мы увеличиваем размер пула потоков. Ожидается ли это? или, я просто делаю что-то глупое?

enter image description here

Программа тестирования

    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<DateFormat> dfWithTZ = new ThreadLocal<DateFormat>() {
        @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
4b9b3361

Ответ 1

SimpleDateFormat Не поддерживается потоками

Как гласит правильный ответ Мартина Уилсона, создание экземпляра SimpleDateFormat относительно дорого.

Зная, что ваша первая мысль может быть "Ну, пусть кэширует экземпляр для повторного использования". Приятно подумать, но будьте осторожны: класс SimpleDateFormat в не в потоковом режиме. Так говорит документация по классу под заголовком "Синхронизация".

Joda времени

Лучшим решением является избежание печально известных (и теперь устаревших) классов java.util.Date,.Calendar и SimpleDateFormat. Вместо этого используйте либо:

  • Joda-Time
    сторонняя библиотека с открытым исходным кодом, популярная замена Date/Calendar.
  • пакет java.time
    Новое, в комплекте Java 8, вытесняя старые классы Date/Calendar, вдохновленные Joda-Time, определенные JSR 310.

Joda-Time намеренно построено на потокобезопасность, в основном благодаря использованию неизменяемых объектов. Существуют некоторые изменчивые классы, но обычно они не используются.

Этот другой вопрос о StackOverflow объясняет, что класс DateTimeFormatter действительно потокобезопасен. Таким образом, вы можете создать один экземпляр, кешировать его и позволить всем вашим потокам использовать этот форматтер без добавления дополнительной синхронизации или других элементов управления concurrency.

Ответ 2

Создание экземпляра SimpleDateFormat очень дорого (в этой статье показано некоторое профилирование/бенчмаркинг). Если это так, по сравнению с синтаксическим разбором дат в строках, то из этого следует, что по мере увеличения количества потоков (и, следовательно, количества экземпляров SimpleDateFormat, поскольку они являются threadlocals) ваше среднее время будет увеличиваться.

Ответ 3

Другой подход к ускорению форматирования - кэширование форматированного результата. Это учитывает тот факт, что, как правило, не так много разных дат для форматирования. Если вы разделите формат даты и времени, это даже лучший кандидат для кэширования.

Недостатком этого является то, что обычные реализации кэша Java, такие как EHCache, замедляются, кэш-доступ занимает больше времени, чем форматирование.

Существует еще одна реализация кэша, которая имеет время доступа на уровне с HashMap. В этом случае вы получите хорошую скорость. Здесь вы найдете мое доказательство концептуальных тестов: https://github.com/headissue/cache2k-benchmark/blob/master/zoo/src/test/java/org/cache2k/benchmark/DateFormattingBenchmark.java

Возможно, это может быть решением в вашем сценарии.

Отказ от ответственности: я работаю над cache2k....

Ответ 4

Наш usecase писал один раз (один поток) и читал много раз (одновременно). Поэтому я преобразовал Date в String во время хранения данных, вместо того чтобы делать это каждый раз, когда запрос должен отвечать.