SimpleDateFormatクラスのセキュリティ問題、これらの6つのソリューションの1つは常にあなたに適しています

この記事は、Huawei Cloud Community " [High Concurrency] Thread Safety Issues and Solutions of SimpleDateFormat Class(with 6 Solutions) "、作成者:Glacierから共有されています。

まず、皆さんに聞いてみましょう。使用しているSimpleDateFormatクラスはまだ安全ですか?SimpleDateFormatクラスがスレッドセーフではないのはなぜですか?この記事で答えを見つけるためにあなたの質問を持ってきてください。

SimpleDateFormatクラスに関しては、Java開発を行ったことのある人は誰もなじみがないと感じることはありません。はい、Javaで提供されている日付と時刻の変換クラスです。ここで、SimpleDateFormatクラスにスレッドセーフの問題があると言うのはなぜですか?一部の友人は質問をするかもしれません:私たちはSimpleDateFormatクラスを使用して、本番環境で日付と時刻のデータを解析およびフォーマットしてきましたが、問題はありませんでした。私の答えは次のとおりです。はい、SimpleDateFormatクラスで問題が発生する同時実行性がシステムにないためです。つまり、システムに負荷がかかっていません。

次に、SimpleDateFormatクラスに高い同時実行性の下でセキュリティの問題がある理由と、SimpleDateFormatクラスのセキュリティの問題を解決する方法を見てみましょう。

SimpleDateFormatクラスでスレッドセーフの問題を再現する

SimpleDateFormatクラスのスレッドセーフの問題を再現するための比較的簡単な方法は、JavaコンカレントパッケージのCountDownLatchクラスおよびSemaphoreクラスと組み合わせたスレッドプールを使用して、スレッドセーフの問題を再現することです。

CountDownLatchクラスとSemaphoreクラスの特定の使用法、基本原則、およびソースコード分析については、後の[高並行性トピック]で詳しく分析します。ここで必要なのは、CountDownLatchクラスが、他のスレッドが実行する前に他のスレッドが相互に実行するのをスレッドに待機させることができるということだけです。セマフォクラスは、それを取得するスレッドによって解放される必要があるカウントセマフォとして理解できます。これは、電流制限など、特定のリソースにアクセスするスレッドの数を制限するためによく使用されます。

それでは、以下に示すように、SimpleDateFormatクラスのスレッドセーフの問題を再現するコードを見てみましょう。

package io.binghe.concurrent.lab06;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @author binghe
 * @version 1.0.0
 * @description 测试SimpleDateFormat的线程不安全问题
 */
public class SimpleDateFormatTest01 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        simpleDateFormat.parse("2020-01-01");
                    } catch (ParseException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }catch (NumberFormatException e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

SimpleDateFormatTest01クラスでは、最初に2つの定数が定義され、1つはプログラム実行の総数であり、もう1つは同時に実行されているスレッドの数であることがわかります。このプログラムは、スレッドプール、CountDownLatchクラス、およびSemaphoreクラスを組み合わせて、同時実行性の高いビジネスシナリオをシミュレートします。その中で、日付変換に関連するコードは次の行だけです。

simpleDateFormat.parse("2020-01-01");

プログラムが例外をキャッチすると、関連情報を出力してプログラム全体を終了します。プログラムが正しく実行されると、「すべてのスレッドが日付を正常にフォーマットしました」と出力されます。

プログラムを実行して出力される結果情報を以下に示します。

Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" 线程:pool-1-thread-7 格式化日期失败
线程:pool-1-thread-9 格式化日期失败
线程:pool-1-thread-10 格式化日期失败
Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-6" 线程:pool-1-thread-15 格式化日期失败
线程:pool-1-thread-21 格式化日期失败
Exception in thread "pool-1-thread-23" 线程:pool-1-thread-16 格式化日期失败
线程:pool-1-thread-11 格式化日期失败
java.lang.ArrayIndexOutOfBoundsException
线程:pool-1-thread-27 格式化日期失败
	at java.lang.System.arraycopy(Native Method)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597)
	at java.lang.StringBuffer.append(StringBuffer.java:367)
	at java.text.DigitList.getLong(DigitList.java:191)线程:pool-1-thread-25 格式化日期失败

	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
线程:pool-1-thread-14 格式化日期失败
	at java.text.DateFormat.parse(DateFormat.java:364)
	at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
线程:pool-1-thread-13 格式化日期失败	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)

	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
线程:pool-1-thread-20 格式化日期失败	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:2084)
	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 io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
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:2084)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)

Process finished with exit code 1

SimpleDateFormatクラスを使用して高い同時実行性で日付をフォーマットすると例外がスローされ、SimpleDateFormatクラスはスレッドセーフではないことに注意してください。

次に、SimpleDateFormatクラスがスレッドセーフではない理由を見てみましょう。

SimpleDateFormatクラスがスレッドセーフではないのはなぜですか?

それでは、次に、SimpleDateFormatクラスのスレッドの不安定さの根本的な原因を見てみましょう。

SimpleDateFormatクラスのソースコードを見ると、次のことがわかります。SimpleDateFormatはDateFormatクラスを継承し、DateFormatクラスは以下に示すようにグローバルCalendar変数を維持します。

/**
  * The {@link Calendar} instance used for calculating the date-time fields
  * and the instant of time. This field is used for both formatting and
  * parsing.
  *
  * <p>Subclasses should initialize this field to a {@link Calendar}
  * appropriate for the {@link Locale} associated with this
  * <code>DateFormat</code>.
  * @serial
  */
protected Calendar calendar;

コメントからわかるように、このCalendarオブジェクトは、日時のフォーマットと解析の両方に使用されます。次に、最後の方にあるparse()メソッドを見てみましょう。

@Override
public Date parse(String text, ParsePosition pos){
    ################此处省略N行代码##################
    Date parsedDate;
    try {
        parsedDate = calb.establish(calendar).getTime();
        // If the year value is ambiguous,
        // then the two-digit year == the default start year
        if (ambiguousYear[0]) {
            if (parsedDate.before(defaultCenturyStart)) {
                parsedDate = calb.addYear(100).establish(calendar).getTime();
            }
        }
    }
    // An IllegalArgumentException will be thrown by Calendar.getTime()
    // if any fields are out of range, e.g., MONTH == 17.
    catch (IllegalArgumentException e) {
        pos.errorIndex = start;
        pos.index = oldStart;
        return null;
    }
    return parsedDate;
}

最終的な戻り値はCalendarBuilder.establish()メソッドを呼び出すことによって取得され、このメソッドのパラメーターは正確に前のCalendarオブジェクトであることがわかります。

次に、以下に示すように、CalendarBuilder.establish()メソッドを見てみましょう。

Calendar establish(Calendar cal) {
    boolean weekDate = isSet(WEEK_YEAR)
        && field[WEEK_YEAR] > field[YEAR];
    if (weekDate && !cal.isWeekDateSupported()) {
        // Use YEAR instead
        if (!isSet(YEAR)) {
            set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
        }
        weekDate = false;
    }

    cal.clear();
    // Set the fields from the min stamp to the max stamp so that
    // the field resolution works in the Calendar.
    for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
        for (int index = 0; index <= maxFieldIndex; index++) {
            if (field[index] == stamp) {
                cal.set(index, field[MAX_FIELD + index]);
                break;
            }
        }
    }

    if (weekDate) {
        int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
        int dayOfWeek = isSet(DAY_OF_WEEK) ?
            field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
        if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
            if (dayOfWeek >= 8) {
                dayOfWeek--;
                weekOfYear += dayOfWeek / 7;
                dayOfWeek = (dayOfWeek % 7) + 1;
            } else {
                while (dayOfWeek <= 0) {
                    dayOfWeek += 7;
                    weekOfYear--;
                }
            }
            dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
        }
        cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
    }
    return cal;
}

CalendarBuilder.establish()メソッドでは、cal.clear()とcal.set()が連続して呼び出されます。つまり、最初にcalオブジェクトに設定された値がクリアされ、次に新しい値がリセットされます。Calendar内にはスレッドセーフメカニズムがなく、これら2つの操作はアトミックではないため、複数のスレッドが同時にSimpleDateFormatを操作すると、calの値が混乱します。同様に、  format()メソッドにも同じ問題があります。

したがって、SimpleDateFormatクラスがスレッドセーフではない根本的な理由は、DateFormatクラスのCalendarオブジェクトが複数のスレッドで共有されており、Calendarオブジェクト自体がスレッドセーフをサポートしていないためです。

したがって、SimpleDateFormatクラスがスレッドセーフではないこと、およびSimpleDateFormatクラスがスレッドセーフではない理由を知っている場合、この問題を解決するにはどうすればよいでしょうか。次に、同時実行性の高いシナリオでSimpleDateFormatクラスのスレッドセーフの問題を解決する方法について説明します。

SimpleDateFormatクラスのスレッドセーフの問題を解決します

同時実行性の高いシナリオでSimpleDateFormatクラスのスレッドセーフの問題を解決する方法はたくさんあります。ここでは、参照用に一般的に使用されるメソッドをいくつか示します。コメント領域にさらに解決策を示すこともできます。

1.ローカル変数メソッド

最も簡単な方法は、以下のコードに示すように、SimpleDateFormatクラスオブジェクトをローカル変数として定義し、問題を解決するためにparse(String)メソッドの上にSimpleDateFormatクラスオブジェクトを定義することです。

package io.binghe.concurrent.lab06;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @author binghe
 * @version 1.0.0
 * @description 局部变量法解决SimpleDateFormat类的线程安全问题
 */
public class SimpleDateFormatTest02 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
                        simpleDateFormat.parse("2020-01-01");
                    } catch (ParseException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }catch (NumberFormatException e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

この時点で、変更したプログラムを実行すると、出力は次のようになります。

所有线程格式化日期成功

同時実行性の高いシナリオでローカル変数を使用することでスレッドセーフの問題を解決できる理由については、[JVMトピック]のJVMメモリモード関連のコンテンツで詳細に分析します。ここではあまり紹介しません。

もちろん、このメソッドは高い同時実行性で多数のSimpleDateFormatクラスオブジェクトを作成し、プログラムのパフォーマンスに影響を与えるため、このメソッドは実際の実稼働環境では推奨されません。

2.同期ロック方式

SimpleDateFormatクラスオブジェクトをグローバル静的変数として定義します。このとき、すべてのスレッドがSimpleDateFormatクラスオブジェクトを共有します。このとき、フォーマット時間のメソッドを呼び出すと、SimpleDateFormatオブジェクトを同期できます。コードは次のとおりです。

package io.binghe.concurrent.lab06;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @author binghe
 * @version 1.0.0
 * @description 通过Synchronized锁解决SimpleDateFormat类的线程安全问题
 */
public class SimpleDateFormatTest03 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        synchronized (simpleDateFormat){
                            simpleDateFormat.parse("2020-01-01");
                        }
                    } catch (ParseException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }catch (NumberFormatException e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

この時点で、問題を解決するためのキーコードを以下に示します。

synchronized (simpleDateFormat){
	simpleDateFormat.parse("2020-01-01");
}

プログラムを実行すると、出力は次のようになります。

所有线程格式化日期成功

このメソッドはSimpleDateFormatクラスのスレッドセーフの問題を解決できますが、プログラムの実行中に同期ロックがSimpleDateFormatクラスオブジェクトに追加されるため、同時にparse(String)を実行できるのは1つのスレッドのみであることに注意してください。 。メソッド。現時点では、プログラムの実行パフォーマンスに影響があります。高い同時実行性が必要な本番環境では、この方法はお勧めしません。

3.ロックロック方式

ロック方式は同期ロック方式と同じであり、どちらも高い同時実行性の下でJVMロックメカニズムを介してプログラムのスレッドセーフを保証します。Locklock方式で問題を解決するためのコードは次のとおりです。

package io.binghe.concurrent.lab06;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author binghe
 * @version 1.0.0
 * @description 通过Lock锁解决SimpleDateFormat类的线程安全问题
 */
public class SimpleDateFormatTest04 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    //Lock对象
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        lock.lock();
                        simpleDateFormat.parse("2020-01-01");
                    } catch (ParseException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }catch (NumberFormatException e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }finally {
                        lock.unlock();
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

コードから、まず、ロックタイプのグローバル静的変数がロックおよびロック解除のハンドルとして定義されていることがわかります。次に、simpleDateFormat.parse(String)コードの前にlock.lock()でロックします。ここで注意すべきことの1つは、プログラムが例外をスローしてロックを解放できないようにするために、以下に示すように、ロックを解放する操作をfinallyコードブロックに配置する必要があることです。

finally {
	lock.unlock();
}

プログラムを実行すると、出力は次のようになります。

所有线程格式化日期成功

この方法は、同時実行性の高いシナリオでのパフォーマンスにも影響を与えるため、同時実行性の高い本番環境で使用することはお勧めしません。

4.ThreadLocalメソッド

ThreadLocalを使用して各スレッドが所有するSimpleDateFormatオブジェクトのコピーを格納すると、マルチスレッドによって引き起こされるスレッドセーフの問題を効果的に回避できます。スレッドセーフの問題を解決するためにThreadLocalを使用するためのコードは次のとおりです。

package io.binghe.concurrent.lab06;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @author binghe
 * @version 1.0.0
 * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题
 */
public class SimpleDateFormatTest05 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;

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

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        threadLocal.get().parse("2020-01-01");
                    } catch (ParseException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }catch (NumberFormatException e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

コードから、各スレッドで使用されるSimpleDateFormatのコピーがThreadLocalに保存され、使用時に各スレッドが相互に干渉しないため、スレッドセーフの問題が解決されることがわかります。

プログラムを実行すると、出力は次のようになります。

所有线程格式化日期成功

この方法は運用効率が高く、同時実行のビジネスシナリオが多い本番環境に推奨されます。

さらに、ThreadLocalを使用すると、次の形式のコードで記述することもできますが、効果は同じです。

package io.binghe.concurrent.lab06;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @author binghe
 * @version 1.0.0
 * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题
 */
public class SimpleDateFormatTest06 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();

    private static DateFormat getDateFormat(){
        DateFormat dateFormat = threadLocal.get();
        if(dateFormat == null){
            dateFormat = new SimpleDateFormat("yyyy-MM-dd");
            threadLocal.set(dateFormat);
        }
        return dateFormat;
    }

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        getDateFormat().parse("2020-01-01");
                    } catch (ParseException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }catch (NumberFormatException e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

5.DateTimeFormatterメソッド

DateTimeFormatterは、Java8によって提供される新しい日付と時刻のAPIのクラスです。DateTimeFormatterクラスはスレッドセーフであり、同時実行性の高いシナリオで日付の書式設定操作を処理するために直接使用できます。コードを以下に示します。

package io.binghe.concurrent.lab06;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @author binghe
 * @version 1.0.0
 * @description 通过DateTimeFormatter类解决线程安全问题
 */
public class SimpleDateFormatTest07 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;

   private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        LocalDate.parse("2020-01-01", formatter);
                    }catch (Exception e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

DateTimeFormatterクラスはスレッドセーフであり、DateTimeFormatterクラスを直接使用して、同時実行性の高いシナリオで日付フォーマット操作を処理できることがわかります。

プログラムを実行すると、出力は次のようになります。

所有线程格式化日期成功

DateTimeFormatterクラスを使用して日付フォーマット操作を処理することは比較的効率的であり、同時実行性の高いビジネスシナリオのある実稼働環境に推奨されます

6.joda-timeメソッド

joda-timeは、日時のフォーマットを処理し、スレッドセーフなサードパーティのライブラリです。joda-timeを使用して日付と時刻のフォーマットを処理する場合は、サードパーティのライブラリをインポートする必要があります。ここでは、Mavenを例にとると、joda-timeライブラリは次のようにインポートされます。

<dependency>
	<groupId>joda-time</groupId>
	<artifactId>joda-time</artifactId>
	<version>2.9.9</version>
</dependency>

joda-timeライブラリの導入後、実装されたプログラムコードは次のとおりです。

package io.binghe.concurrent.lab06;

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @author binghe
 * @version 1.0.0
 * @description 通过DateTimeFormatter类解决线程安全问题
 */
public class SimpleDateFormatTest08 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;

    private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        DateTime.parse("2020-01-01", dateTimeFormatter).toDate();
                    }catch (Exception e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

ここで、以下に示すように、DateTimeクラスはorg.joda.timeパッケージの下のクラスであり、DateTimeFormatクラスとDateTimeFormatterクラスは両方ともorg.joda.time.formatパッケージの下のクラスであることに注意してください。

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

プログラムを実行すると、出力は次のようになります。

所有线程格式化日期成功

joda-timeライブラリを使用して日付フォーマット操作を処理することは比較的効率的であり、同時実行性の高いビジネスシナリオのある実稼働環境での使用をお勧めします。

SimpleDateFormatクラスのスレッドセーフ問題の解決策の要約

要約すると、SimpleDateFormatクラスのスレッドセーフの問題を解決するいくつかのソリューションでは、ローカル変数メソッドは、スレッドがフォーマット時間を実行するたびにSimpleDateFormatクラスのオブジェクトを作成します。これにより、多数のSimpleDateFormatオブジェクト、無駄な実行スペース、およびオブジェクトの破壊はパフォーマンスを大量に消費するため、サーバーのパフォーマンスを消費します。したがって、同時実行性の要件が高い本番環境で使用することはお勧めしません

同期ロック方式とロックロック方式は基本的に同じですが、ロックすることで、日付と時刻のフォーマット操作を同時に実行できるのは1つのスレッドだけです。このメソッドはSimpleDateFormatオブジェクトの作成を減らしますが、同期ロックが存在するためにパフォーマンスが低下するため、同時実行性の要件が高い実稼働環境で使用することはお勧めしません。

ThreadLocalは、各スレッドのSimpleDateFormatクラスオブジェクトのコピーを保存するため、各スレッドは、実行時に互いに干渉することなく独自のバインドされたSimpleDateFormatオブジェクトを使用し、実行パフォーマンスが比較的高くなります。同時実行性の高い場所で使用することをお勧めします。本番環境。

DateTimeFormatterは、日付と時刻を処理するためにJava 8で提供されるクラスです。DateTimeFormatterクラス自体はスレッドセーフです。ストレステスト後、日付と時刻の処理におけるDateTimeFormatterクラスのパフォーマンスは悪くありません(パフォーマンスストレスに関する別の記事を作成します)。後で高い同時実行性の下で。テスト記事)。したがって、同時実行性の高いシナリオの実稼働環境で使用することをお勧めします。

joda-timeは、日付と時刻を処理するためのサードパーティのクラスライブラリです。スレッドセーフであり、同時実行性の高いテストに合格しています。同時実行性の高いシナリオの本番環境に推奨されます

初めてHUAWEICLOUDの新技術について学ぶには、[フォロー]をクリックしてください〜

おすすめ

転載: blog.csdn.net/devcloud/article/details/124144394