なぜアリババはSimpleDateFormatをstaticタイプと定義することを禁止したのですか?

9596 ワード

日常開発では、時間をよく使います.Javaコードで時間を取得する方法がたくさんあります.しかし、異なる方法で取得された時間のフォーマットは異なり、この場合、時間を必要なフォーマットに表示するフォーマットツールが必要です.
最も一般的な方法はSimpleDateFormatクラスを使用することです.これは機能が比較的簡単に見えるクラスですが、不適切に使用すると大きな問題を引き起こす可能性があります.
アリババJava開発マニュアルには、次のような明確な規定があります.
そこで,SimpleDateFormatの使い方,原理などをめぐって,正しい姿勢でどのように使うかを深く分析した.
SimpleDateFormatの使い方
SimpleDateFormatはJavaが提供する日付のフォーマットと解析のツールクラスです.フォーマット(日付->テキスト)、解析(テキスト->日付)、正規化が可能です.SimpleDateFormatにより、任意のユーザー定義の日付-時間フォーマットのモードを選択できます.
Javaでは、SimpleDateFormatのformatメソッドを使用して、DateタイプをStringタイプに変換し、出力フォーマットを指定できます.
// Date String
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);

以上のコードでは、変換の結果、2018-11-25 13:00:00、日付および時刻フォーマットは「日付および時刻モード」文字列で指定されます.他のフォーマットに変換したい場合は、異なる時間モードを指定すればいいです.
Javaでは、SimpleDateFormatのparseメソッドを使用して、StringタイプをDateタイプに変換できます.
// String Data
System.out.println(sdf.parse(dataStr));

日付モードと時刻モードの表現方法
SimpleDateFormatを使用する場合は、時間要素をアルファベットで記述し、希望する日付と時間モードに組み立てる必要があります.一般的な時間要素とアルファベットの対応表は次のとおりです.

パターンアルファベットは通常重複しており、その数は正確な表現を決定します.以下の表が一般的な出力形式の表示方法です.

異なるタイムゾーンを出力する時間
タイムゾーンは地球上の領域で同じ時間定義を使用します.従来、太陽の位置(時角)を観察することによって時間が決定され、経度の異なる場所の時間が異なる(場所時).1863年、タイムゾーンの概念が初めて使用された.タイムゾーンは一つのエリアを設立する標準時間によって部分的にこの問題を解決した.
世界の各国は地球の異なる位置に位置しているので、異なる国、特に東西のスパンの大きい国は日の出、日没の時間が必ずずれている.これらの偏差はいわゆる時差です.
現在、世界は24のタイムゾーンに分かれている.実用的には1つの国、または1つの省が同時に2つ以上のタイムゾーンをまたいでいることが多いため、行政上の便利さを配慮するために、1つの国または1つの省を一緒に画定することが多い.だからタイムゾーンは厳密に南北直線で区切られるのではなく、自然条件で区切られています.例えば、中国は幅が広く、5つのタイムゾーンにまたがる差は多くないが、使いやすいように、実際には東八時区の標準時である北京時間だけを基準にしている.
タイムゾーンによって時間が異なり、同じ国の都市によっても時間が異なる可能性があるので、Javaで時間を取得したいときは、タイムゾーンの問題に重点を置いてみましょう.
デフォルトでは、作成日を指定しないと、現在のコンピュータが存在するタイムゾーンがデフォルトタイムゾーンとして使用されます.これも、new Date()を使用するだけで中国の現在の時間を取得できる理由です.
では、Javaコードで異なるタイムゾーンの時間を取得するにはどうすればいいのでしょうか.SimpleDateFormatはこの機能を実現することができます.
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));

以上のコードは、変換の結果、2018-11-24 21:00:00です.中国時間は11月25日の13時だが、米ロサンゼルス時間は中国の北京時間より16時間遅い(これは冬と夏の時間と関係があり、詳しくは展開されない).
興味があれば、アメリカのニューヨーク時間(America/New_York)を印刷してみてください.ニューヨーク時間は2018-11-25 00:00です.ニューヨーク時間は中国の北京時間より13時間早いです.
もちろん、これは他のタイムゾーンを表示する唯一の方法ではありませんが、本稿では主にSimpleDateFormatを紹介するために、他の方法はしばらく紹介しません.
SimpleDateFormatスレッドセキュリティ
SimpleDateFormatは一般的によく使われており、一般的には1つのアプリケーションの時間表示モードが同じであるため、SimpleDateFormatを次のように定義したい人が多い.
public class Main {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
        System.out.println(simpleDateFormat.format(Calendar.getInstance().getTime()));
    }
}

このような定義方式には、大きな安全上の危険性がある.
問題の再現
スレッドプールを使用して時間出力を実行するコードを見てみましょう.
   /** * @author Hollis */ 
   public class Main {

    /**
     *        SimpleDateFormat
     */
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     *   ThreadFactoryBuilder       
     */
    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    /**
     *     CountDownLatch,                  
     */
    private static CountDownLatch countDownLatch = new CountDownLatch(100);

    public static void main(String[] args) {
        //         HashSet
        Set dates = Collections.synchronizedSet(new HashSet());
        for (int i = 0; i < 100; i++) {
            //      
            Calendar calendar = Calendar.getInstance();
            int finalI = i;
            pool.execute(() -> {
                    //    
                    calendar.add(Calendar.DATE, finalI);
                    //  simpleDateFormat         
                    String dateString = simpleDateFormat.format(calendar.getTime());
                    //      Set 
                    dates.add(dateString);
                    //countDown
                    countDownLatch.countDown();
            });
        }
        //  ,  countDown   0
        countDownLatch.await();
        //          
        System.out.println(dates.size());
    }
}

以上のコードは、実は簡単で、理解しやすいです.つまり、100回ループし、ループするたびに現在の時間に基づいて1つの日数(この日数はループ回数によって変化する)を増やし、すべての日付をスレッドが安全で、重み付け機能のあるSetに入れ、Setの要素の個数を出力します.
上の例はわざわざ少し複雑に書きましたが、ほとんど注釈をつけました.スレッドプールの作成、CountDownLatch、lambda式、スレッドセキュリティのHashSetなどの知識が含まれています.興味のある友達は一つ一つ理解することができます.
通常、上記のコード出力結果は100であるべきです.しかし、実際の実行結果は100未満の数字です.
なぜならSimpleDateFormatは非スレッドセキュリティクラスとして共有変数として複数のスレッドで使用されているため、スレッドセキュリティの問題が発生している.
アリババJava開発マニュアルの第1章第6節-同時処理では、この点についても明確に説明されています.
では、次に、なぜ、どのように解決すべきかを見てみましょう.
スレッドが安全でない理由
以上のコードから,同時シーンでSimpleDateFormatを使用するとスレッドセキュリティの問題があることが分かった.実際、JDKドキュメントでは、SimpleDateFormatがマルチスレッドシーンで使用されるべきではないことが明らかになっています.
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.
では、なぜこのような問題が発生したのか、SimpleDateFormatの下層はどのように実現されたのかを分析します.
SimpleDateFormatクラスにおけるformatメソッドの実装について説明すると,実際には手がかりが見つかる.

SimpleDateFormatのformatメソッドは、実行中にメンバー変数calendarを使用して時間を保存します.これが問題の鍵だ.
SimpleDateFormatを宣言するときにstatic定義を使用するためです.このSimpleDateFormatは共有変数であり、SimpleDateFormatのcalendarも複数のスレッドにアクセスできます.
スレッド1が実行されたばかりのcalendar.setTimeが時間を2018-11-11に設定し、まだ実行が完了していないと仮定し、スレッド2はまたcalendar.setTimeを実行して時間を2018-12-12に変更した.このときスレッド1は引き続き実行され、取得したcalendar.getTimeが取得した時間はスレッド2が変更された後である.
format法に加えてSimpleDateFormatのparse法にも同様の問題がある.
したがって、SimpleDateFormatを共有変数として使用しないでください.
解決方法
前にSimpleDateFormatに存在する問題と問題の原因を紹介しましたが、この問題を解決する方法は何でしょうか.
解決策はいろいろありますが、ここでは3つの比較的よく使われる方法を紹介します.
ローカル変数の使用
for (int i = 0; i < 100; i++) {
    //      
    Calendar calendar = Calendar.getInstance();
    int finalI = i;
    pool.execute(() -> {
        // SimpleDateFormat       
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //    
        calendar.add(Calendar.DATE, finalI);
        //  simpleDateFormat         
        String dateString = simpleDateFormat.format(calendar.getTime());
        //      Set 
        dates.add(dateString);
        //countDown
        countDownLatch.countDown();
    });
}

SimpleDateFormatがローカル変数になると、複数のスレッドに同時にアクセスされなくなり、スレッドセキュリティの問題が回避されます.
同期ロックをかける
ローカル変数に変更する以外に、共有変数にロックをかける方法もよく知られているかもしれません.
for (int i = 0; i < 100; i++) {
    //      
    Calendar calendar = Calendar.getInstance();
    int finalI = i;
    pool.execute(() -> {
        //  
        synchronized (simpleDateFormat) {
            //    
            calendar.add(Calendar.DATE, finalI);
            //  simpleDateFormat         
            String dateString = simpleDateFormat.format(calendar.getTime());
            //      Set 
            dates.add(dateString);
            //countDown
            countDownLatch.countDown();
        }
    });
}

ロックをかけることで、複数のスレッドを順番に並べて実行します.同時発生によるスレッドセキュリティの問題を回避します.
実は以上のコードには、ロックの粒度をもう少し小さく設定して、simpleDateFormat.format行だけをロックすることができて、効率がもっと高いという改善点があります.
ThreadLocalの使用
3つ目の方法は、ThreadLocalを使用することです.ThreadLocalは、各スレッドが個別のSimpleDateFormatのオブジェクトを得ることができることを確保することができ、競争問題は自然に存在しません.
/**
 *   ThreadLocal       SimpleDateFormat
 */
private static ThreadLocal simpleDateFormatThreadLocal = new ThreadLocal() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

//  
String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime());

ThreadLocalで実現するのはキャッシュに似た考え方で、各スレッドには独自のオブジェクトがあり、頻繁にオブジェクトを作成することを回避し、マルチスレッドの競争も回避します.
もちろん、上記のコードにも改善の余地があります.つまり、SimpleDateFormatの作成プロセスを遅延ロードに変更することができます.ここでは詳しく紹介しません.
DateTimeFormatterの使用
Java 8アプリケーションの場合、SimpleDateFormatの代わりにDateTimeFormatterを使用できます.これはスレッドの安全なフォーマットツールクラスです.公式ドキュメントで述べたように、このクラスsimple beautiful strong immutable thread-safeです.
//    
String dateStr= "2016 10 25 ";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd ");
LocalDate date= LocalDate.parse(dateStr, formatter);

//        
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy MM dd  hh:mm a");
String nowStr = now .format(format);
System.out.println(nowStr);

まとめ
ここではSimpleDateFormatの使い方を紹介し,SimpleDateFormatは主にStringとDateの間で変換することができ,時間を異なるタイムゾーン出力に変換することもできる.同時に,SimpleDateFormatは同時シーンではスレッドのセキュリティを保証できないため,開発者自身がセキュリティを保証する必要があると述べた.
主ないくつかの手段は、ローカル変数への変更、synchronizedロックの使用、Threadlocalを使用してスレッドごとに1つずつ作成するなどである.
この文を通じて、SimpleDateFormatを使うときにもっとうまくいくことを望んでいます.