Синхронизация доступа к SimpleDateFormat

В javadoc для SimpleDateFormat указано, что SimpleDateFormat не синхронизирован.

"Форматы даты не синхронизированы. рекомендуется создавать отдельные для каждого потока. Если несколько потоков обращаются к формату одновременно, он должен быть синхронизирован извне".

Но каков наилучший подход к использованию экземпляра SimpleDateFormat в многопоточной среде. Вот несколько вариантов, о которых я подумал, я использовал варианты 1 и 2 в прошлом, но мне любопытно узнать, есть ли какие-то лучшие альтернативы или какие из этих вариантов будут предлагать лучшую производительность и concurrency.

Вариант 1: при необходимости создайте локальные экземпляры

public String formatDate(Date d) {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    return sdf.format(d);
}

Вариант 2. Создайте экземпляр SimpleDateFormat в качестве переменной класса, но синхронизируйте с ним доступ.

private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public String formatDate(Date d) {
    synchronized(sdf) {
        return sdf.format(d);
    }
}

Вариант 3. Создайте ThreadLocal для хранения другого экземпляра SimpleDateFormat для каждого потока.

private ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public String formatDate(Date d) {
    SimpleDateFormat sdf = tl.get();
    if(sdf == null) {
        sdf = new SimpleDateFormat("yyyy-MM-hh");
        tl.set(sdf);
    }
    return sdf.format(d);
}

Ответ 1

  • Создание SimpleDateFormat дорогой. Не используйте это, если это не делается редко.

  • ОК, если вы можете жить с некоторой блокировкой. Используйте, если formatDate() не используется много.

  • Самый быстрый вариант, если вы повторно используете потоки (пул потоков). Использует больше памяти, чем 2. и имеет более высокую начальную нагрузку.

Для приложений оба варианта 2. и 3. являются жизнеспособными. Что лучше для вашего дела, зависит от вашего варианта использования. Остерегайтесь преждевременной оптимизации. Только делайте это, если считаете, что это проблема.

Для библиотек, которые будут использоваться сторонней стороной, я бы использовал опцию 3.

Ответ 2

Другой вариант - Commons Lang FastDateFormat, но вы можете использовать его только для форматирования даты, а не разбор.

В отличие от Joda, он может функционировать как замена для форматирования. (Обновление: начиная с версии 3.3.2, FastDateFormat может создавать FastDateParser, который является заменой потоковой замены для SimpleDateFormat)

Ответ 3

Если вы используете Java 8, вы можете использовать java.time.format.DateTimeFormatter:

Этот класс является неизменным и потокобезопасным.

например:.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String str = new java.util.Date().toInstant()
                                 .atZone(ZoneId.systemDefault())
                                 .format(formatter);

Ответ 4

В Commons Lang 3.x теперь есть FastDateParser, а также FastDateFormat. Он потокобезопасен и быстрее, чем SimpleDateFormat. Он также использует те же спецификации формата/описания разбора, что и SimpleDateFormat.

Ответ 5

Не используйте SimpleDateFormat, вместо этого используйте DateDimeFormatter вместо joda. Это немного сложнее в синтаксическом анализе, поэтому замена SimpleDateFormat не является полной заменой, но joda-time гораздо более дружелюбен с точки зрения безопасности и производительности.

Ответ 6

Я бы сказал, создайте простой класс-оболочку для SimpleDateFormat, который синхронизирует доступ к parse() и format() и может быть использован в качестве замены. Более надежный, чем ваш вариант № 2, менее громоздкий, чем ваш вариант № 3.

Похоже, что создание SimpleDateFormat несинхронизировано - это плохое дизайнерское решение со стороны разработчиков Java API; Я сомневаюсь, что кто-то ожидает, что формат() и parse() должны быть синхронизированы.

Ответ 7

Другим вариантом является сохранение экземпляров в потокобезопасной очереди:

import java.util.concurrent.ArrayBlockingQueue;
private static final int DATE_FORMAT_QUEUE_LEN = 4;
private static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";
private ArrayBlockingQueue<SimpleDateFormat> dateFormatQueue = new ArrayBlockingQueue<SimpleDateFormat>(DATE_FORMAT_QUEUE_LEN);
// thread-safe date time formatting
public String format(Date date) {
    SimpleDateFormat fmt = dateFormatQueue.poll();
    if (fmt == null) {
        fmt = new SimpleDateFormat(DATE_PATTERN);
    }
    String text = fmt.format(date);
    dateFormatQueue.offer(fmt);
    return text;
}
public Date parse(String text) throws ParseException {
    SimpleDateFormat fmt = dateFormatQueue.poll();
    if (fmt == null) {
        fmt = new SimpleDateFormat(DATE_PATTERN);
    }
    Date date = null;
    try {
        date = fmt.parse(text);
    } finally {
        dateFormatQueue.offer(fmt);
    }
    return date;
}

Размер dateFormatQueue должен быть чем-то близким к предполагаемому числу потоков, которые могут одновременно вызвать эту функцию одновременно. В худшем случае, когда большее количество потоков, чем это число, фактически использует все экземпляры одновременно, будут созданы экземпляры SimpleDateFormat, которые не могут быть возвращены dateFormatQueue, потому что они заполнены. Это не приведет к ошибке, это приведет к штрафу за создание некоторого SimpleDateFormat, который используется только один раз.

Ответ 8

Представьте, что ваше приложение имеет один поток. Зачем вам синхронизировать доступ к переменной SimpleDataFormat?

Ответ 9

Я только что реализовал это с помощью опции 3, но сделал несколько изменений кода:

  • ThreadLocal обычно должен быть статическим.
  • Кажется, что очистить, чтобы переопределить initialValue(), а не проверить, если (get() == null)
  • Возможно, вы захотите установить языковой стандарт и часовой пояс, если вы действительно не хотите, чтобы настройки по умолчанию (по умолчанию очень подвержены ошибкам Java)

    private static final ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-hh", Locale.US);
            sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
            return sdf;
        }
    };
    public String formatDate(Date d) {
        return tl.get().format(d);
    }