web-dev-qa-db-ja.com

JavaのSimpleDateFormatがスレッドセーフではないのはなぜですか?

SimpleDateFormatがスレッドセーフではない理由をコード例を示してください。このクラスの問題は何ですか? SimpleDateFormatのフォーマット機能に問題はありますか?クラスでこの障害を示すコードを教えてください。

FastDateFormatはスレッドセーフです。どうして? SimpleDateFormatとFastDateFormatの違いは何ですか?

この問題を実証するコードで説明してください?

217
Vivek Sharma

SimpleDateFormatは、中間結果をインスタンスフィールドに保存します。したがって、1つのインスタンスが2つのスレッドで使用されている場合、互いの結果が台無しになります。

ソースコード を見ると、Calendarインスタンスフィールドがあり、DateFormat/SimpleDateFormatの操作で使用されていることがわかります。

たとえば、parse(..)は最初にcalendar.clear()を呼び出し、次にcalendar.add(..)を呼び出します。最初の呼び出しが完了する前に別のスレッドがparse(..)を呼び出すと、カレンダーはクリアされますが、他のスレッドは計算の中間結果が取り込まれることを期待します。

スレッドセーフを交換せずに日付形式を再利用する1つの方法は、それらをThreadLocalに入れることです-一部のライブラリはそれを行います。 1つのスレッド内で同じ形式を複数回使用する必要がある場合です。ただし、(スレッドプールを持つ)サーブレットコンテナを使用している場合は、終了後にスレッドローカルを必ず削除してください。

正直に言うと、インスタンスフィールドが必要な理由はわかりませんが、それはそうです。 joda-timeDateTimeFormatを使用することもできます。これはスレッドセーフです。

236
Bozho

SimpleDateFormat は、ロケールに依存した方法で日付をフォーマットおよび解析するための具象クラスです。

JavaDoc から、

ただし、日付形式はnot synchronizedです。スレッドごとに個別の形式インスタンスを作成することをお勧めします。複数のスレッドが同時にフォーマットにアクセスする場合は、it must be synchronized externally

SimpleDateFormatクラスをスレッドセーフにするには、 following approach を見てください:

  • 使用する必要があるたびに、新しいSimpleDateFormatインスタンスを作成します。これはスレッドセーフですが、可能な限り最も遅いアプローチです。
  • 同期を使用します。サーバー上でスレッドをチョークポイントすることは絶対にしないでください。
  • ThreadLocalを使用します。これは3つの最速のアプローチです( http://www.javacodegeeks.com/2010/07/Java-best-practices-dateformat-in.html を参照)。
59
sgokhales

Java 8の DateTimeFormatterNAME _ は、 SimpleDateFormatNAME _ の不変でスレッドセーフな代替です。

36
TheKojuEffect

ThreadLocal + SimpleDateFormat = SimpleDateFormatThreadSafe

package com.foocoders.text;

import Java.text.AttributedCharacterIterator;
import Java.text.DateFormatSymbols;
import Java.text.FieldPosition;
import Java.text.NumberFormat;
import Java.text.ParseException;
import Java.text.ParsePosition;
import Java.text.SimpleDateFormat;
import Java.util.Calendar;
import Java.util.Date;
import Java.util.Locale;
import Java.util.TimeZone;

public class SimpleDateFormatThreadSafe extends SimpleDateFormat {

    private static final long serialVersionUID = 5448371898056188202L;
    ThreadLocal<SimpleDateFormat> localSimpleDateFormat;

    public SimpleDateFormatThreadSafe() {
        super();
        localSimpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat();
            }
        };
    }

    public SimpleDateFormatThreadSafe(final String pattern) {
        super(pattern);
        localSimpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(pattern);
            }
        };
    }

    public SimpleDateFormatThreadSafe(final String pattern, final DateFormatSymbols formatSymbols) {
        super(pattern, formatSymbols);
        localSimpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(pattern, formatSymbols);
            }
        };
    }

    public SimpleDateFormatThreadSafe(final String pattern, final Locale locale) {
        super(pattern, locale);
        localSimpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(pattern, locale);
            }
        };
    }

    public Object parseObject(String source) throws ParseException {
        return localSimpleDateFormat.get().parseObject(source);
    }

    public String toString() {
        return localSimpleDateFormat.get().toString();
    }

    public Date parse(String source) throws ParseException {
        return localSimpleDateFormat.get().parse(source);
    }

    public Object parseObject(String source, ParsePosition pos) {
        return localSimpleDateFormat.get().parseObject(source, pos);
    }

    public void setCalendar(Calendar newCalendar) {
        localSimpleDateFormat.get().setCalendar(newCalendar);
    }

    public Calendar getCalendar() {
        return localSimpleDateFormat.get().getCalendar();
    }

    public void setNumberFormat(NumberFormat newNumberFormat) {
        localSimpleDateFormat.get().setNumberFormat(newNumberFormat);
    }

    public NumberFormat getNumberFormat() {
        return localSimpleDateFormat.get().getNumberFormat();
    }

    public void setTimeZone(TimeZone zone) {
        localSimpleDateFormat.get().setTimeZone(zone);
    }

    public TimeZone getTimeZone() {
        return localSimpleDateFormat.get().getTimeZone();
    }

    public void setLenient(boolean lenient) {
        localSimpleDateFormat.get().setLenient(lenient);
    }

    public boolean isLenient() {
        return localSimpleDateFormat.get().isLenient();
    }

    public void set2DigitYearStart(Date startDate) {
        localSimpleDateFormat.get().set2DigitYearStart(startDate);
    }

    public Date get2DigitYearStart() {
        return localSimpleDateFormat.get().get2DigitYearStart();
    }

    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
        return localSimpleDateFormat.get().format(date, toAppendTo, pos);
    }

    public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
        return localSimpleDateFormat.get().formatToCharacterIterator(obj);
    }

    public Date parse(String text, ParsePosition pos) {
        return localSimpleDateFormat.get().parse(text, pos);
    }

    public String toPattern() {
        return localSimpleDateFormat.get().toPattern();
    }

    public String toLocalizedPattern() {
        return localSimpleDateFormat.get().toLocalizedPattern();
    }

    public void applyPattern(String pattern) {
        localSimpleDateFormat.get().applyPattern(pattern);
    }

    public void applyLocalizedPattern(String pattern) {
        localSimpleDateFormat.get().applyLocalizedPattern(pattern);
    }

    public DateFormatSymbols getDateFormatSymbols() {
        return localSimpleDateFormat.get().getDateFormatSymbols();
    }

    public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) {
        localSimpleDateFormat.get().setDateFormatSymbols(newFormatSymbols);
    }

    public Object clone() {
        return localSimpleDateFormat.get().clone();
    }

    public int hashCode() {
        return localSimpleDateFormat.get().hashCode();
    }

    public boolean equals(Object obj) {
        return localSimpleDateFormat.get().equals(obj);
    }

}

https://Gist.github.com/pablomoretti/97482

33
Pablo Moretti

commons-langのリリース3.2には、グレゴリオ暦のFastDateParserのスレッドセーフな代替であるSimpleDateFormatクラスが含まれます。詳細については、 LANG-909 を参照してください。

15
dma_k

奇妙なエラーが発生する例を次に示します。 Googleでさえ結果を出しません:

public class ExampleClass {

private static final Pattern dateCreateP = Pattern.compile("Дата подачи:\\s*(.+)");
private static final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss dd.MM.yyyy");

public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(100);
    while (true) {
        executor.submit(new Runnable() {
            @Override
            public void run() {
                workConcurrently();
            }
        });
    }
}

public static void workConcurrently() {
    Matcher matcher = dateCreateP.matcher("Дата подачи: 19:30:55 03.05.2015");
    Timestamp startAdvDate = null;
    try {
        if (matcher.find()) {
            String dateCreate = matcher.group(1);
            startAdvDate = new Timestamp(sdf.parse(dateCreate).getTime());
        }
    } catch (Throwable th) {
        th.printStackTrace();
    }
    System.out.print("OK ");
}
}

そして結果:

OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK Java.lang.NumberFormatException: For input string: ".201519E.2015192E2"
at Sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.Java:2043)
at Sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.Java:110)
at Java.lang.Double.parseDouble(Double.Java:538)
at Java.text.DigitList.getDouble(DigitList.Java:169)
at Java.text.DecimalFormat.parse(DecimalFormat.Java:2056)
at Java.text.SimpleDateFormat.subParse(SimpleDateFormat.Java:1869)
at Java.text.SimpleDateFormat.parse(SimpleDateFormat.Java:1514)
at Java.text.DateFormat.parse(DateFormat.Java:364)
at com.nonscalper.webscraper.processor.av.ExampleClass.workConcurrently(ExampleClass.Java:37)
at com.nonscalper.webscraper.processor.av.ExampleClass$1.run(ExampleClass.Java:25)
at Java.util.concurrent.Executors$RunnableAdapter.call(Executors.Java:511)
at Java.util.concurrent.FutureTask.run(FutureTask.Java:266)
at Java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.Java:1142)
at Java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.Java:617)
at Java.lang.Thread.run(Thread.Java:745)
9
user2602807

これが コード例 です。これはクラスの障害を証明します。私はチェックしました:解析を使用しているとき、およびフォーマットのみを使用しているときにも問題が発生します。

4

SimpleDateFormatオブジェクトを静的フィールドとして定義する例を次に示します。 2つ以上のスレッドが異なる日付で「someMethod」に同時にアクセスすると、互いの結果を混乱させる可能性があります。

    public class SimpleDateFormatExample {
         private static final SimpleDateFormat simpleFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

         public String someMethod(Date date) {
            return simpleFormat.format(date);
         }
    }

以下のようなサービスを作成し、jmeterを使用して、異なる日付をフォーマットする同じSimpleDateFormatオブジェクトを使用する同時ユーザーをシミュレートすると、結果が台無しになります。

public class FormattedTimeHandler extends AbstractHandler {

private static final String OUTPUT_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
private static final String INPUT_TIME_FORMAT = "yyyy-MM-ddHH:mm:ss";
private static final SimpleDateFormat simpleFormat = new SimpleDateFormat(OUTPUT_TIME_FORMAT);
// Apache commons lang3 FastDateFormat is threadsafe
private static final FastDateFormat fastFormat = FastDateFormat.getInstance(OUTPUT_TIME_FORMAT);

public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

    response.setContentType("text/html;charset=utf-8");
    response.setStatus(HttpServletResponse.SC_OK);
    baseRequest.setHandled(true);

    final String inputTime = request.getParameter("time");
    Date date = LocalDateTime.parse(inputTime, DateTimeFormat.forPattern(INPUT_TIME_FORMAT)).toDate();

    final String method = request.getParameter("method");
    if ("SimpleDateFormat".equalsIgnoreCase(method)) {
        // use SimpleDateFormat as a static constant field, not thread safe
        response.getWriter().println(simpleFormat.format(date));
    } else if ("FastDateFormat".equalsIgnoreCase(method)) {
        // use Apache commons lang3 FastDateFormat, thread safe
        response.getWriter().println(fastFormat.format(date));
    } else {
        // create new SimpleDateFormat instance when formatting date, thread safe
        response.getWriter().println(new SimpleDateFormat(OUTPUT_TIME_FORMAT).format(date));
    }
}

public static void main(String[] args) throws Exception {
    // embedded jetty configuration, running on port 8090. change it as needed.
    Server server = new Server(8090);
    server.setHandler(new FormattedTimeHandler());

    server.start();
    server.join();
}

}

コードとjmeterスクリプトは here からダウンロードできます。

3
ylu