SimpleDateFormat スレッド安全性問題修復ソリューション | JD Logistics 技術チーム

問題の紹介

日々の開発プロセスでは、必然的に JDK8 以前の Date クラスを使用することになり、日付の書式設定や日付の解析の際には SimpleDateFormat クラスを使用する必要がありますが、このクラスはスレッドセーフではないため、このクラスの不適切な使用が頻繁に発生します。クラスにより日付解析例外が発生し、オンライン サービスの可用性に影響します。

以下は、SimpleDateFormat クラスの不適切な使用のサンプル コードです。

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全问题复现
 * @Version: V1.0
 **/
public class SimpleDateFormatTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-M-d"); // 非线程安全

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    FORMATTER.parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}


上記のコードは、SimpleDateFormat インスタンスが複数のスレッドによって同時に使用されるシナリオをシミュレートします。このとき、次の異常な出力が観察されます。

java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
	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.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	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:2087)
	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.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	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:2087)
	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.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
	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.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)


上記の例外の根本的な原因は、 SimpleDateFormat がステートフルであることです。たとえば、SimpleDateFormat クラスには、スレッド セーフではないNumberFormat メンバー変数が含まれています。

/**
 * The number formatter that <code>DateFormat</code> uses to format numbers
 * in dates and times.  Subclasses should initialize this to a number format
 * appropriate for the locale associated with this <code>DateFormat</code>.
 * @serial
 */
protected NumberFormat numberFormat;


NumberFormat のJava ドキュメントから、次の説明を参照できます。

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

SimpleDateFormat のJava ドキュメントから、次の説明を確認できます。

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

解決策 1: ロックする (推奨されません)

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全修复方案:加锁
 * @Version: V1.0
 **/
public class SimpleDateFormatLockTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-M-d"); // 非线程安全

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    synchronized (FORMATTER) {
                        FORMATTER.parse("2023-7-15");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}


まず、スレッド セーフティの問題を解決するために考えられる最も簡単な修復ソリューションはロックです。上記の修復計画では、synchronized キーワードを使用して FORMATTER インスタンスをロックします。このとき、マルチスレッドで日付の書式設定が実行されると、シリアル実行に縮退します。正確性は保証されますが、パフォーマンスが犠牲になるため、推奨されません。

解決策 2: スタックの閉鎖 (推奨されません)

ドキュメントで推奨されている使用法に従うと、スレッドごとに独立した SimpleDateFormat インスタンスを作成することが推奨されていることがわかります。最も簡単な方法の 1 つは、スタックの効果を達成するためにメソッドが呼び出されるたびに SimpleDateFormat インスタンスを作成することです。次のサンプルコードに示すように、クロージャを使用します。

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全修复方案:栈封闭
 * @Version: V1.0
 **/
public class SimpleDateFormatStackConfinementTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    new SimpleDateFormat("yyyy-M-d").parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}


共有 SimpleDateFormat インスタンスは、毎回新しいインスタンスを作成するように調整されます。この修正により正確さが保証されますが、各メソッド呼び出しで SimpleDateFormat インスタンスを作成する必要があり、SimpleDateFormat インスタンスは再利用されません。GC 損失があるため、お勧めできませ

解決策 3: ThreadLocal (推奨)

日付フォーマット操作がアプリケーションで高頻度の操作であり、パフォーマンスを最初に保証する必要がある場合は、各スレッドで SimpleDateFormat インスタンスを再利用することをお勧めします。このとき、ThreadLocal クラスを導入してこの問題を解決できます。

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全修复方案:ThreadLocal
 * @Version: V1.0
 **/
public class SimpleDateFormatThreadLocalTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-M-d"));

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}


上記のコードを実行すると、異常な出力は観察されなくなります。独立した SimpleDateFormat インスタンスがスレッドごとに作成されているためです。つまり、SimpleDateFormat インスタンスはスレッド ディメンションで再利用され、スレッド プールなどのプーリング シナリオこのスキーム

スレッド ディメンションで非スレッド セーフ インスタンスを再利用するための上記の ThreadLocal の使用は、一般的なパターンと考えることができ、同様のパターンは JDK や多くのオープン ソース プロジェクトで見られます。たとえば、JDK の最も一般的な String クラスで見られます。 、文字の場合 文字列のエンコードとデコードに必要な StringDecoder と StringEncoder は、スレッド セーフティの問題を回避するために ThreadLocal を使用します。

/**
 * Utility class for string encoding and decoding.
 */
class StringCoding {

    private StringCoding() { }

    /** The cached coders for each thread */
    private final static ThreadLocal<SoftReference<StringDecoder>> decoder =
        new ThreadLocal<>();
    private final static ThreadLocal<SoftReference<StringEncoder>> encoder =
        new ThreadLocal<>();

    // ...
}


参考:JDK8 - StringCoding

Dubbo の ThreadLocalKryoFactory クラスでは、スレッド セーフでない Kryo クラスを使用する際に、スレッド セーフの問題を回避するために ThreadLocal クラスも使用されます。

package org.apache.dubbo.common.serialize.kryo.utils;

import com.esotericsoftware.kryo.Kryo;

public class ThreadLocalKryoFactory extends AbstractKryoFactory {

    private final ThreadLocal<Kryo> holder = new ThreadLocal<Kryo>() {
        @Override
        protected Kryo initialValue() {
            return create();
        }
    };

    @Override
    public void returnKryo(Kryo kryo) {
        // do nothing
    }

    @Override
    public Kryo getKryo() {
        return holder.get();
    }
}


参考:Dubbo - ThreadLocalKryoFactory

同様に、 HikariCP のConcurrentBagクラスでも、スレッド セーフティの問題を回避するために ThreadLocal クラスが使用されるため、ここではこれ以上拡張しません。

解決策 4: FastDateFormat (推奨)

SimpleDateFormat クラスのスレッド安全性の問題に対して、Apache commons-lang は FastDateFormat クラスを提供しています。Java ドキュメントの一部は次のとおりです。

FastDateFormat は、高速でスレッドセーフな のバージョンですSimpleDateFormatFastDateFormat のインスタンスを取得するには、静的ファクトリ メソッドのいずれかを使用します。 、 、 、getInstance(String, TimeZone, Locale)またはgetDateInstance(int, TimeZone, Locale)FastDateFormatgetTimeInstance(int, TimeZone, Locale)getDateTimeInstance(int, int, TimeZone, Locale)スレッド セーフであるため、静的メンバー インスタンスを使用できます。 private static Final FastDateFormat DATE_FORMATTER = FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.SHORT) ); このクラスは、ほとんどの書式設定および解析状況での直接の置き換えとして使用できますSimpleDateFormatこのクラスは、マルチスレッド サーバー環境で特に役立ちます。SimpleDateFormatどの JDK バージョンでもスレッドセーフではありません。また、Sun がバグ/RFE をクローズしたため、スレッドセーフでもありません。すべてのパターンは SimpleDateFormat と互換性があります (タイム ゾーンと一部の年のパターンを除く - 以下を参照)。

この修復ソリューションはコードの変更が比較的最小限で、静的な SimpleDateFormat インスタンス コードが宣言されている場所で SimpleDateFormat インスタンスを FastDateFormat インスタンスに置き換えるだけで済みます。サンプル コードは次のとおりです。

package com.jd.threadsafe;

import org.apache.commons.lang3.time.FastDateFormat;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/6 20:05
 * @Desc: SimpleDateFormat 线程安全修复方案:FastDateFormat
 * @Version: V1.0
 **/
public class FastDateFormatTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final FastDateFormat FORMATTER = FastDateFormat.getInstance("yyyy-M-d");

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    FORMATTER.parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}


FastDateFormat はマルチスレッドの同時呼び出しをサポートするスレッドセーフな実装であるため、上記のコードを実行しても異常な出力は観察されなくなります。

要約する

どの修復ソリューションを使用する場合でも、この修復が正確であることを確認するために、単体テストやトラフィックの再生など、修復後に元のビジネス ロジックが影響を受けないことを確認するために、変更後に十分なテストを行う必要があります。

考える

コード内で SimpleDateFormat クラスが使用される理由は、日付には Date クラスが使用され、Dateに一致するJDK 書式設定クラスは SimpleDateFormat クラスであるためです。サポートされているスレッドセーフなDateTimeFormatter クラスにより、ルートからの非スレッドセーフの SimpleDateFormat クラスの使用が回避されます。

著者: JD Logistics Liu Jianshe Zhang Jiulong Tian Shuang

出典: JD Cloud 開発者コミュニティによる Yuanqishuo Tech からの転載。出典を明記してください

インド国防省が自社開発した Maya OS は、Windows Redis 7.2.0 を完全に置き換えるもので、最も広範囲にわたるバージョンの 7-Zip 公式 Web サイトが、Baidu によって悪意のある Web サイトであると特定されました 。 Xiaomi がCyber​​Dog 2をリリース、オープンソース率80%以上 ChatGPTの1日コスト約70万ドル、OpenAIが破産寸前の可能性 瞑想ソフトが上場へ、「中国初のLinux人」が設立 Apache Doris 2.0.0版正式リリース: ブラインド テストのパフォーマンスが 10 倍向上、より統合され多様な超高速分析エクスペリエンス Linux カーネル (v0.01) のオープン ソース コード解釈の最初のバージョン Chrome 116 が正式リリース
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/4090830/blog/10098343