web-dev-qa-db-ja.com

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

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

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

これが原因ですか:

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

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

139
haps10

試してみましょう。

これは、複数のスレッドが共有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;
  }
}

ここに良い post があります。

252
dogbane

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

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

私はexactDateFormatがスレッドセーフではない理由の詳細を調べていませんが、私にとってはそれで十分ですitis同期なしでは安全ではありません-安全でない方法の正確な方法はリリース間で変わる可能性があります。

個人的には、代わりに Joda Time のパーサーを使用します。これは、areスレッドセーフであるためです。で始まるAPIと時間:)

29
Jon Skeet

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
12
cjungel

大まかに言うと、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());  
    }
}

また、すべての場合において、staticDateFormatを持たないでください

Jon Skeetが述べたように、外部同期を実行する場合(つまり、synchronizedへの呼び出しの周りでDateFormatを使用する場合)に、静的インスタンス変数と共有インスタンス変数の両方を使用できます。

10
Bozho

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

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

何らかの方法でDateFormatを操作する必要がある場合は、何かを行う必要があります

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

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

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

1
Buhake Sindi

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

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

1
Michał Niklas

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

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

  • newFixedThreadPoolを5に設定したままにすると、ループは毎回失敗します。
  • 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");
}
1
Nux

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

0
seand

これは、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
0
Erangad