SimpleDateFormat非スレッドセキュリティの問題

12699 ワード

皆さんはSimpleDateFormatに慣れていないと思います.SimpleDateFormatはJavaで非常に一般的なクラスです.このクラスは日付文字列を解析して出力をフォーマットするために使用されますが、うっかり使用すると、DateFormatとSimpleDateFormatクラスはスレッドが安全ではなく、マルチスレッド環境でformat()とparse()を呼び出すため、非常に微妙でデバッグしにくい問題が発生します.メソッドは、問題を回避するために同期コードを使用する必要があります.次に、SimpleDateFormatクラスを具体的なシーンで一歩一歩深く学び、理解します.
1.リード私たちは優秀なプログラマーで、プログラムの中でSimpleDateFormatインスタンスをできるだけ少なく作成しなければならないことを知っています.このようなインスタンスを作成するには大きな代価がかかるからです.データベースデータを読み込んでexcelファイルにエクスポートする例では、時間情報を処理するたびにSimpleDateFormatインスタンスオブジェクトを作成し、そのオブジェクトを破棄する必要があります.大量のオブジェクトが作成され、大量のメモリとjvm空間が消費されます.コードは次のとおりです.
package com.peidasoft.dateformat;

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

public class DateUtil {
    
    public static  String formatDate(Date date)throws ParseException{
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }
    
    public static Date parse(String strDate) throws ParseException{
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.parse(strDate);
    }
}

 OKと言うかもしれませんが、静的simpleDateFormatインスタンスを作成し、DateUtilクラス(以下)に配置して、使用時にこのインスタンスを直接使用して操作すると、問題が解決します.改善されたコードは次のとおりです.
package com.peidasoft.dateformat;

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

public class DateUtil {
    private static final  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    public static  String formatDate(Date date)throws ParseException{
        return sdf.format(date);
    }
    
    public static Date parse(String strDate) throws ParseException{

        return sdf.parse(strDate);
    }
}

 もちろん、この方法は確かによくて、ほとんどの時間の中でよく働いています.しかし、本番環境でしばらく使用すると、スレッドが安全ではないという事実に気づきます.通常のテストでは問題ありませんが、生産環境で一定の負荷がかかると、この問題が発生します.変換の時間が正しくないなど、スレッドが切られているなど、さまざまな状況が発生します.次のテスト例を見てみましょう
package cg.zzbj.test;

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

/**
 * Created by FuHaitao on 2014/12/14.
 */
public class DateUtil {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String formatDate(Date date) throws ParseException{
        return sdf.format(date);
    }

    public static Date parse(String strDate) throws ParseException{
        return sdf.parse(strDate);
    }
}

 
package cg.zzbj.test;

import java.text.ParseException;

/**
 * Created by FuHaitao on 2014/12/14.
 */
public class DateUtilTest {

    public static class TestSimpleDateFormatThreadSafe extends Thread{
        @Override
        public void run(){
            while (true){
                try {
                    this.join(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    System.out.println(this.getName() + ":" + DateUtil.parse("2014-12-14 16:10:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args){
        for (int i = 0; i < 3; i++) {
            new TestSimpleDateFormatThreadSafe().start();
        }
    }
}

 実行出力は次のとおりです.
Exception in thread "Thread-1" 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:1936)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)
	at java.text.DateFormat.parse(DateFormat.java:335)
	at cg.zzbj.test.DateUtil.parse(DateUtil.java:18)
	at cg.zzbj.test.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Thread-0:Fri Dec 14 16:10:20 CST 2012
Thread-2:Fri Dec 14 16:10:20 CST 2012
Thread-0:Sun Dec 14 16:10:20 CST 2014
Thread-2:Sat Dec 14 16:10:20 CST 20
Thread-0:Sun Dec 14 16:10:20 CST 2014
Thread-2:Sun Dec 14 16:10:20 CST 2014

 説明:Thread-1とThread-0報java.lang.NumberFormatException:multiple pointsエラー、直接死んで、起きていません;Thread-2は死んでいませんが、出力の時間は間違っています.例えば、入力した時間は2013-05-24 06:02:20で、Mon May 24 06:02:20 CST 2021のような霊的なイベントが出力されます.
二.原因
 
プロのプログラマーとして、変数を共有するよりも、新しい変数を作成するたびにコストがかかることは当然知っています.上の最適化された静的SimpleDateFormat版は、SimpleDateFormatクラスとDateFormatクラスがスレッドセキュリティではないため、同時に様々な霊的なエラーが発生します.スレッドセキュリティの問題を無視しているのは、SimpleDateFormatクラスとDateFormatクラスが提供してくれたインタフェースから見ると、スレッドセキュリティと何の関係があるのか分からないからです.ただし、JDKドキュメントの一番下には次のような説明があります.
 
SimpleDateFormatの日付フォーマットは同期されていません.各スレッドに独立したフォーマットインスタンスを作成することを推奨します(推奨).複数のスレッドが同時に1つのフォーマットにアクセスする場合は、外部同期を維持する必要があります.
 
JDKの元のドキュメントは次のとおりです:Synchronization:Date formats are not synchronized.It is recommended to create separate format instances for each thread.If multiple threads access a format concurrently,it must be synchronized externally.
 
次に、JDKソースコードを見て、SimpleDateFormatクラスとDateFormatクラスがスレッドセキュリティの本当の原因ではない理由を見てみましょう.
 
SimpleDateFormatはDateFormatを継承し、DateFormatでprotectedプロパティのCalendarクラスのオブジェクト:calendarを定義します.ただし、Calendarの複雑な概念のため、タイムゾーンやローカリゼーションなどに関連し、Jdkの実装ではメンバー変数を使用してパラメータを伝達するため、マルチスレッドの場合にエラーが発生します.
 
formatメソッドには、次のコードがあります.
 private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);

    boolean useDateFormatSymbols = useDateFormatSymbols();

        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
        count = compiledPattern[i++] << 16;
        count |= compiledPattern[i++];
        }

        switch (tag) {
        case TAG_QUOTE_ASCII_CHAR:
        toAppendTo.append((char)count);
        break;

        case TAG_QUOTE_CHARS:
        toAppendTo.append(compiledPattern, i, count);
        i += count;
        break;

        default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
        break;
        }
    }
        return toAppendTo;
    }

 calendar.setTime(date)という文はcalendarを変更し、後でcalendarは(subFormatメソッドで)使用しますが、これが問題の根源です.1つのマルチスレッド環境では、同じSimpleDateFormatのインスタンスを持つ2つのスレッドがあり、formatメソッドをそれぞれ呼び出します.スレッド1はformatメソッドを呼び出し、calendarというフィールドを変更します.中断した.スレッド2は実行を開始し、calendarも変更されました.また途切れた.スレッド1が戻ってきたとき、calendarはすでに設定された値ではなく、スレッド2設計の道を歩んでいた.複数のスレッドがcalendarオブジェクトを同時に争うと、時間の違い、スレッドの停止など、さまざまな問題が発生します.formatの実装を分析すると、メンバー変数calendarを使用する唯一の利点は、subFormatを呼び出すときにパラメータが1つ足りないのに、多くの問題をもたらすことです.実は、ここで局所変数を使って、一緒に伝えれば、すべての問題が解決されます.この問題の背後には、ステータスなし:ステータスレスメソッドのメリットの1つとして、さまざまな環境で安全に呼び出すことができるというより重要な問題が隠されています.メソッドがステータスであるかどうかを測定するには、インスタンスのフィールドなどのグローバル変数などの他のものを変更したかどうかを見ます.formatメソッドは実行中にSimpleDateFormatのcalendarフィールドを変更したので、ステータスがあります.
これは、システムの開発と設計において、次の3つの点に注意することを同時に示しています.
 
1.自分で共通クラスを書くときは、マルチスレッド呼び出しの場合の結果をコメントで明確に説明する
 
2.スレッド環境では、共有する変数ごとにスレッドセキュリティに注意する
 
3.私たちの類と方法は設計をする時、できるだけ無状態に設計しなければならない.
 
三.解決方法
1.必要に応じて新しいインスタンスを作成します.
package com.peidasoft.dateformat;

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

public class DateUtil {
    
    public static  String formatDate(Date date)throws ParseException{
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }
    
    public static Date parse(String strDate) throws ParseException{
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.parse(strDate);
    }
}

 説明:SimpleDateFormatを使用する必要がある場所にインスタンスを新規作成すると、スレッドセキュリティの問題があるオブジェクトを共有からローカルプライベートに変更してもマルチスレッドの問題は回避できますが、オブジェクトの作成に負担がかかります.一般的には、パフォーマンスへの影響比は明らかではありません.
2.同期の使用:SimpleDateFormatオブジェクトの同期
package com.peidasoft.dateformat;

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

public class DateSyncUtil {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      
    public static String formatDate(Date date)throws ParseException{
        synchronized(sdf){
            return sdf.format(date);
        }  
    }
    
    public static Date parse(String strDate) throws ParseException{
        synchronized(sdf){
            return sdf.parse(strDate);
        }
    } 
}

 説明:スレッドが多い場合、1つのスレッドがメソッドを呼び出すと、他のメソッドを呼び出すスレッドがblockされ、マルチスレッドの同時量が大きい場合はパフォーマンスに影響します.
3.ThreadLocalを使用する:
package com.peidasoft.dateformat;

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

public class ConcurrentDateUtil {

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }

    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
}

 もう1つの書き方:
package com.peidasoft.dateformat;

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

public class ThreadLocalDateUtil {
    private static final String date_format = "yyyy-MM-dd HH:mm:ss";
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); 
 
    public static DateFormat getDateFormat()   
    {  
        DateFormat df = threadLocal.get();  
        if(df==null){  
            df = new SimpleDateFormat(date_format);  
            threadLocal.set(df);  
        }  
        return df;  
    }  

    public static String formatDate(Date date) throws ParseException {
        return getDateFormat().format(date);
    }

    public static Date parse(String strDate) throws ParseException {
        return getDateFormat().parse(strDate);
    }   
}

 説明:ThreadLocalを使用すると、共有変数もユニークになります.スレッド・ユニークは、メソッド・ユニークよりも同時環境でオブジェクトの作成にかかるコストを削減することができます.性能に対する要求が高い場合は、一般的にこの方法が推奨されます.
4.JDKを捨てて、他のクラスライブラリの時間フォーマットクラスを使用します.
1.Apache commonsのFastDateFormatを使用して、速くてスレッドが安全なSimpleDateFormatだと主張していますが、残念ながら日付をformatするしかなく、日付列を解析することはできません.
2.Joda-timeクラスライブラリを使用して時間に関する問題を処理
   
簡単な圧力テストをして、方法は1番遅くて、方法は3番速くて、しかし最も遅い方法でも性能は悪くなくて、普通のシステムの方法は1番と方法2で満足することができて、だからこの点であなたのシステムのボトルネックになるのは難しいと言っています.簡単な角度から言えば、使用方法1か方法2をお勧めしますが、必要に応じて、それだけの性能向上を追求すれば、方法3で、ThreadLocalでキャッシュすることも考えられます.
Joda-timeクラスライブラリは時間処理が完璧で、使用することをお勧めします.
参考資料:
  1. http://dreamhead.blogbus.com/logs/215637834.html
  2.http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html