Friday, January 14, 2011

Потокобезопасный DateFormat


"Является ли класс java.text.DateFormat потокобезопасным?". Этот вопрос очень любят задавать на собеседовании по вакансии Java-программиста. Ответ на него – нет, не является. Вот только собеседующие редко спрашивают, а как же тогда быть, если все-таки надо его использовать в многопоточном окружении? Меж тем в реальных задачах мало просто знать, что DateFormat не потокобезопасен, было бы неплохо еще и знать способ решения проблемы. Сравнению возможных вариантов решения и посвящен данный пост.

Элементарная логика и гугл родили на свет пять вариантов:
  1. Вариант решения "в лоб". Если DateFormat не потокобезопасный, давайте синхронизируем доступ. А чтобы не писать каждый раз блокировки вручную, воспользуемся паттерном декоратор. В точности, как это реализовано в Collections.synchronizedList() итп.
  2. Другой, не менее очевидный вариант, давайте создавать новый экземпляр класса на каждый вызов.
  3. Воспользуемся ThreadLocal для хранения одного экземпляра DateFormat на поток.
  4. Воспользуемся классом из Apache Commons Lang FastDateFormat. Пожалуй основной недостаток этого варианта – он не поддерживает парсинг даты. Поддерживается только печать.
  5. И последний, но отнюдь не маловажный вариант – использовать библиотечку joda-time. К сожалению, эта библиотека поддерживает не все паттерны стандартного DateFormat поэтому, не во всех случаях будет возможно на нее переключиться.

Что ж, пусть тест производительности решит вопрос и расставит наконец точки над i. В данном тесте проверялась скорость выполнения 100000 операций парсинга и форматирования в каждом из 20 потоков работающих одновременно.Тест запускался дважды, первый раз с ключиком JVM -server, второй раз с ключем -client.

Полный исходный код теста можно скачать здесь. Для запуска, так же, понадобятся последние версии библиотек Apache Commons Lang и Joda Time.

Результаты получились следующие:
Время парсинга (мс)Время форматирования (мс)
Synchronized java.text.DateFormat12110 / 137513484 / 2766
New instance per call java.text.DateFormat4954 / 102343750 / 7546
ThreadLocal java.text.DateFormat1703 / 2687766 / 656
Apache FastDateFormatНе поддерживается1500 / 2297
Joda Time1062 / 1828500 / 1047
SimpleDateFormat*1697 / 2656758 / 652
* - это референсное время, в данном случае мы эффективно используем DateFormat в однопоточном режиме. Создав для каждого из 20 потоков собственный экземпляр класса SimpleDateFormat.

Первое время - это время выполнения для серверной JVM, второе время - для клиентской JVM.


В случае серверной версии, с впечатляющим отрывом лидирует Joda  Time,  обгоняя даже референсную версию от SimpleDateFormat.

В случае форматирования при клиентской версии JVM в отрыв уходит версия с ThreadLocal.

Может быть, читатель знает какие-то еще варианты решения этой задачи, буду рад их услышать и сравнить с имеющимися.

6 comments:

  1. Было бы интересно увидеть в табличке референсный вариант вообще без оверхеда. Ну я имею ввиду в каждом из 20 потоков создать свой инстанс и дергать его (без пересоздания в каждый раз). По идее получиться что-то очень близкое к ThreadLocal варианту, но все же там есть оверхед на конкррент доступ к мапе, где лежат объекты.

    ReplyDelete
  2. Спасибо за совет, добавил эту версию в таблицу. Действительно она очень близка к значениям с ThreadLocal. Кроме того, увеличил в 10 раз количество циклов и запустил в двух режимах работы JVM (сервер и клиент), что внезапно привело к смене лидера.

    ReplyDelete
  3. А что у тебя за процессор? Ведь, на одноядерной тачке по идее первый и последний вариант в случае с -server должны быть примерно одни и те же.

    ReplyDelete
  4. Intel Xeon 5130, 4 ядра. Надо будет проверить на ноутбуке.
    Написал и понял, что дожили, теперь уже не так просто найти однопроцессорную машинку для проверки :)

    ReplyDelete
  5. А если клонировать?
    Это возможно быстрее чем New instance per call

    ReplyDelete
  6. Не проверял, но, признаться, не думаю, что клонирование принципиально изменит рейтинг.

    ReplyDelete