「Java DateFormatはスレッドセーフではありません」これは何につながりますか?


143

Java DateFormatがスレッドセーフではないことについて誰もが警告し、概念を理論的に理解しています。

しかし、これが原因で私たちが直面する可能性のある実際の問題を視覚化することはできません。たとえば、クラスにDateFormatフィールドがあり、マルチスレッド環境のクラスの異なるメソッド(日付のフォーマット)で同じものが使用されているとします。

これが原因ですか:

  • フォーマット例外などの例外
  • データの不一致
  • 他の問題?

また、その理由を説明してください。


1
:これは、につながるものですstackoverflow.com/questions/14309607/...
CAW

現在2020年です。テストを(並行して)実行したところ、あるスレッドから日付をフォーマットしようとすると、あるスレッドの日付がさりげなく返されることがわかりました。コンストラクターがカレンダーをインスタンス化するフォーマッターで見つかり、カレンダーが後でフォーマットする日付を取るように構成されるまで、それが何に依存するかを調査するために数週間かかった。それは彼らの頭の中ではまだ1990年ですか?知るか。
Vlad Patryshev

回答:


262

試してみましょう。

これは、複数のスレッドが共有を使用するプログラムSimpleDateFormatです。

プログラム

public static void main(String[] args) throws Exception {

    final DateFormat format = new SimpleDateFormat("yyyyMMdd");

    Callable<Date> task = new Callable<Date>(){
        public Date call() throws Exception {
            return format.parse("20101022");
        }
    };

    //pool with 5 threads
    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<Date>> results = new ArrayList<Future<Date>>();

    //perform 10 date conversions
    for(int i = 0 ; i < 10 ; i++){
        results.add(exec.submit(task));
    }
    exec.shutdown();

    //look at the results
    for(Future<Date> result : results){
        System.out.println(result.get());
    }
}

これを数回実行すると、次のようになります。

例外

以下にいくつかの例を示します。

1。

Caused by: java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
    at java.lang.Long.parseLong(Long.java:431)
    at java.lang.Long.parseLong(Long.java:468)
    at java.text.DigitList.getLong(DigitList.java:177)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1298)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)

2。

Caused by: java.lang.NumberFormatException: For input string: ".10201E.102014E4"
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1224)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1303)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)

3。

Caused by: java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1084)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1303)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1936)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)

間違った結果

Sat Oct 22 00:00:00 BST 2011
Thu Jan 22 00:00:00 GMT 1970
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Thu Oct 22 00:00:00 GMT 1970
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010

正しい結果

Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010

マルチスレッド環境でDateFormatsを安全に使用するための別のアプローチは、ThreadLocal変数を使用 してDateFormat オブジェクトを保持することです。これは、各スレッドが独自のコピーを持ち、他のスレッドがそれを解放するのを待つ必要がないことを意味します。こうやって:

public class DateFormatTest {

  private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyyMMdd");
    }
  };

  public Date convert(String source) throws ParseException{
    Date d = df.get().parse(source);
    return d;
  }
}

ここでは良いですが、ポストの詳細とは。


1
私はこの答えが大好きです:-)
Sundararaj Govindasamy

これが開発者にとって非常に苛立たしい理由は、一見すると、「機能指向の」関数呼び出しである必要があるように見えるためです。たとえば、同じ入力の場合、同じ出力が期待されます(複数のスレッドがそれを呼び出した場合でも)。私が信じる答えは、Javaの開発者がオリジナルの日時ロジックを記述した時点でFOPを理解していないことに帰着します。つまり、結局のところ、「これが間違っている以外に、このようになっている理由はない」ということです。
Lezorte

30

データの破損が予想されます。たとえば、2つの日付を同時に解析している場合、1つの呼び出しが別の呼び出しのデータによって汚染される可能性があります。

これがどのように発生するかは簡単に想像できます。多くの場合、解析には、これまでに読んだ内容に関する特定の状態を維持することが含まれます。2つのスレッドが両方とも同じ状態で踏みつけている場合、問題が発生します。たとえば、DateFormatはのcalendarタイプのフィールドを公開Calendarし、のコードを見てSimpleDateFormat、一部のメソッド呼び出しcalendar.set(...)と他の呼び出しを呼び出しますcalendar.get(...)。これは明らかにスレッドセーフではありません。

私はに見ていない正確な理由の詳細DateFormatスレッドセーフではありませんが、私のためにそれがあることを知っているだけで十分です同期することなく、安全ではない-非安全の正確なマナーもリリース間で変更することができます。

個人的に私はからのパーサーを使用することになりジョダ時間、彼らのように、代わりにされているスレッドセーフ-とジョダ時間で開始するより良い日時APIです:)


1
その使用を強制する1 jodatimeとソナー:mestachs.wordpress.com/2012/03/17/...
mestachs

18

Java 8を使用している場合は、を使用できますDateTimeFormatter

パターンから作成されたフォーマッターは、必要な回数だけ使用でき、不変であり、スレッドセーフです。

コード:

LocalDate date = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String text = date.format(formatter);
System.out.println(text);

出力:

2017-04-17

10

大まかに言って、DateFormat多くのスレッドがアクセスするオブジェクトのインスタンス変数として、またはを定義すべきではありませんstatic

日付形式は同期されません。スレッドごとに個別のフォーマットインスタンスを作成することをお勧めします。

したがって、次のFoo.handleBar(..)代わりに複数のスレッドによってアクセスされる場合:

public class Foo {
    private DateFormat df = new SimpleDateFormat("dd/mm/yyyy");

    public void handleBar(Bar bar) {
        bar.setFormattedDate(df.format(bar.getStringDate());  
    }
}

あなたは使うべきです:

public class Foo {

    public void handleBar(Bar bar) {
        DateFormat df = new SimpleDateFormat("dd/mm/yyyy");
        bar.setFormattedDate(df.format(bar.getStringDate());  
    }
}

また、すべてのケースで、 static DateFormat

ジョンスキートで述べたように、あなたはケースには、静的および共有インスタンス変数の両方を持つことができますが、外部同期(すなわち使用実行synchronizedへの呼び出しの周りをDateFormat


2
それがまったく続くとは思わない。私は自分のタイプのほとんどをスレッドセーフにしないので、それらのインスタンス変数がスレッドセーフになるとは限りません。静的変数にDateFormatを格納するべきではないと言う方が合理的です。そうでなければ、同期が必要になります。
Jon Skeet、

1
それは、より良い一般的だ-あなたがいる場合、静的にDateFormatを持ってしても大丈夫でしょうがなかった同期を。多くの場合、新しいものをSimpleDateFormat非常に頻繁に作成するよりもパフォーマンスがよくなる可能性があります。それは使用パターンに依存します。
Jon Skeet、

1
静的インスタンスがマルチスレッド環境で問題を引き起こす方法と理由を説明していただけませんか?
Alexandr

4
中間計算をインスタンス変数に保存し、スレッドセーフではないため
Bozho

2

日付形式は同期されません。スレッドごとに個別のフォーマットインスタンスを作成することをお勧めします。複数のスレッドが同時にフォーマットにアクセスする場合は、外部で同期する必要があります。

つまり、DateFormatのオブジェクトがあり、2つの異なるスレッドから同じオブジェクトにアクセスしていて、そのオブジェクトに対してformatメソッドを呼び出し、両方のスレッドが同じオブジェクトで同じメソッドに同時に入るので、それを視覚化できます。適切な結果にならない

DateFormatをどのように操作する必要がある場合は、何かを行う必要があります

public synchronized myFormat(){
// call here actual format method
}

1

データが破損しています。昨日、静的DateFormatオブジェクトがあり、format()JDBCを介して読み取られる値に対してそのオブジェクトを呼び出すマルチスレッドプログラムで、これに気付きました。同じ日付を異なる名前(SELECT date_from, date_from AS date_from1 ...)で読み取るSQL selectステートメントがありました。そのようなステートメントは、WHEREクラスのさまざまな日付の5つのスレッドで使用されていました。日付は「通常」のように見えましたが、値は異なっていました。すべての日付は同じ年のもので、月と日のみが変更されました。

他の回答はそのような腐敗を回避する方法を示しています。私DateFormatは静的ではなく、SQLステートメントを呼び出すクラスのメンバーになりました。静的バージョンも同期してテストしました。どちらもパフォーマンスに違いはなく、うまく機能しました。


1

Format、NumberFormat、DateFormat、MessageFormatなどの仕様は、スレッドセーフになるようには設計されていません。また、parseメソッドはメソッドを呼び出し、Calendar.clone()カレンダーのフットプリントに影響を与えるため、同時に解析する多くのスレッドがCalendarインスタンスの複製を変更します。

さらに、これらはthisthisなどのバグレポートであり、DateFormatスレッド安全性問題の結果が含まれています。


1

最良の回答では、dogbaneがparse関数の使用例とそれがもたらす結果を示しました。以下は、format機能を確認するためのコードです。

エグゼキューター(同時スレッド)の数を変更すると、異なる結果が得られることに注意してください。私の実験から:

  • newFixedThreadPool5に設定したままにすると、ループは毎回失敗します。
  • 1に設定すると、ループは常に機能します(すべてのタスクが実際に1つずつ実行されるため)
  • 2に設定すると、ループは約6%の確率でのみ機能します。

お使いのプロセッサに応じて、YMMVを推測しています。

format関数は、別のスレッドからの時間をフォーマットすることにより、失敗します。これは、内部でformat関数が関数calendarの開始時に設定されたオブジェクトを使用しているためformatです。そして、calendarオブジェクトはSimpleDateFormatクラスのプロパティです。はぁ...

/**
 * Test SimpleDateFormat.format (non) thread-safety.
 *
 * @throws Exception
 */
private static void testFormatterSafety() throws Exception {
    final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    final Calendar calendar1 = new GregorianCalendar(2013,1,28,13,24,56);
    final Calendar calendar2 = new GregorianCalendar(2014,1,28,13,24,56);
    String expected[] = {"2013-02-28 13:24:56", "2014-02-28 13:24:56"};

    Callable<String> task1 = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "0#" + format.format(calendar1.getTime());
        }
    };
    Callable<String> task2 = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "1#" + format.format(calendar2.getTime());
        }
    };

    //pool with X threads
    // note that using more then CPU-threads will not give you a performance boost
    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<String>> results = new ArrayList<>();

    //perform some date conversions
    for (int i = 0; i < 1000; i++) {
        results.add(exec.submit(task1));
        results.add(exec.submit(task2));
    }
    exec.shutdown();

    //look at the results
    for (Future<String> result : results) {
        String answer = result.get();
        String[] split = answer.split("#");
        Integer calendarNo = Integer.parseInt(split[0]);
        String formatted = split[1];
        if (!expected[calendarNo].equals(formatted)) {
            System.out.println("formatted: " + formatted);
            System.out.println("expected: " + expected[calendarNo]);
            System.out.println("answer: " + answer);
            throw new Exception("formatted != expected");
        /**
        } else {
            System.out.println("OK answer: " + answer);
        /**/
        }
    }
    System.out.println("OK: Loop finished");
}

0

単一のDateFormatインスタンスを操作またはアクセスする複数のスレッドがあり、同期が使用されていない場合、スクランブルされた結果が得られる可能性があります。これは、複数の非アトミック操作が状態を変更したり、メモリの表示に一貫性がない可能性があるためです。


0

これは、DateFormatがスレッドセーフではないことを示す簡単なコードです。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class DateTimeChecker {
    static DateFormat df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
    public static void main(String args[]){
       String target1 = "Thu Sep 28 20:29:30 JST 2000";
       String target2 = "Thu Sep 28 20:29:30 JST 2001";
       String target3 = "Thu Sep 28 20:29:30 JST 2002";
       runThread(target1);
       runThread(target2);
       runThread(target3);
   }
   public static void runThread(String target){
       Runnable myRunnable = new Runnable(){
          public void run(){

            Date result = null;
            try {
                result = df.parse(target);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("Ecxfrt");
            }  
            System.out.println(Thread.currentThread().getName() + "  " + result);
         }
       };
       Thread thread = new Thread(myRunnable);

       thread.start();
     }
}

すべてのスレッドが同じSimpleDateFormatオブジェクトを使用しているため、次の例外がスローされます。

Exception in thread "Thread-0" Exception in thread "Thread-2" Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)

しかし、異なるオブジェクトを異なるスレッドに渡した場合、コードはエラーなしで実行されます。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class DateTimeChecker {
    static DateFormat df;
    public static void main(String args[]){
       String target1 = "Thu Sep 28 20:29:30 JST 2000";
       String target2 = "Thu Sep 28 20:29:30 JST 2001";
       String target3 = "Thu Sep 28 20:29:30 JST 2002";
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target1, df);
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target2, df);
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target3, df);
   }
   public static void runThread(String target, DateFormat df){
      Runnable myRunnable = new Runnable(){
        public void run(){

            Date result = null;
            try {
                result = df.parse(target);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("Ecxfrt");
            }  
            System.out.println(Thread.currentThread().getName() + "  " + result);
         }
       };
       Thread thread = new Thread(myRunnable);

       thread.start();
   }
}

これらは結果です。

Thread-0  Thu Sep 28 17:29:30 IST 2000
Thread-2  Sat Sep 28 17:29:30 IST 2002
Thread-1  Fri Sep 28 17:29:30 IST 2001

OPは、なぜこれが起こるのか、また何が起こるのかを尋ねました。
アダム

0

これは ArrayIndexOutOfBoundsException

間違った結果は別として、それはあなたに時々クラッシュを与えるでしょう。それはあなたのマシンの速度に依存します。私のラップトップでは、平均して10万回に1回発生します。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<?> future1 = executorService.submit(() -> {
  for (int i = 0; i < 99000; i++) {
    sdf.format(Date.from(LocalDate.parse("2019-12-31").atStartOfDay().toInstant(UTC)));
  }
});

executorService.submit(() -> {
  for (int i = 0; i < 99000; i++) {
    sdf.format(Date.from(LocalDate.parse("2020-04-17").atStartOfDay().toInstant(UTC)));
  }
});

future1.get();

最後の行は延期されたエグゼキューター例外をトリガーするはずです:

java.lang.ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 13
  at java.base/sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453)
  at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2394)
  at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2309)
  at java.base/java.util.Calendar.complete(Calendar.java:2301)
  at java.base/java.util.Calendar.get(Calendar.java:1856)
  at java.base/java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1150)
  at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:997)
  at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:967)
  at java.base/java.text.DateFormat.format(DateFormat.java:374)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.