SimpleDateFormat
特定の形式の文字を日付に変換したり、日付を特定の形式の文字列に変換したりできる、日付の書式設定のために Java で使用されるクラスであることは誰もが知っています。例えば
- 特定の文字列を日付に変換する
public static void main(String[] args) throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date format = simpleDateFormat.parse("2022-12-19 11:55:56");
System.out.println(format);
}
- 日付を特定の形式の文字列に変換する
public static void main(String[] args) throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// Date format = simpleDateFormat.parse("2022-12-19 11:55:56");
String format = simpleDateFormat.format(new Date());
System.out.println(format);
}
これはすべてシングルスレッドの場合なので問題ありませんが、複数のスレッドが同じSimpleDateFormat
オブジェクトを呼び出すとセキュリティ上の問題が発生します。
スレッド セーフの問題のデモンストレーション (Binghe の記事を参照)
以下のコードでは、SimpleDateFormat オブジェクトが使用される合計回数と、同時に使用できるスレッドの最大数を定義します。その中で、私は Semaphore セマフォを使用して現在を制限しました。つまり、制限されたスレッドの最大数は 20 です。同時に、CountDownLatch を使用して、すべてのスレッドが実行された後もメイン スレッドが実行を継続できるようにします。
package com.dongmu;
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;
/**
* 重现多线程下SimplDateFormat线程不安全的现象
*/
public class SimpleDateFormatTest {
// 定义执行的总次数
private static final int EXECUTE_COUNT = 2000;
// 同时运行的最大的线程数量
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("2022-12-19");
} catch (ParseException e) {
System.out.println("线程"+Thread.currentThread().getName()+"格式化日期失败");
throw new RuntimeException(e);
}catch (NumberFormatException e){
System.out.println("线程"+Thread.currentThread().getName()+"格式化日期失败");
e.printStackTrace();
}
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("所有线程格式化日期完成");
}
}
実行結果が次のようになっていることがわかります。複数のスレッドが同じ SimpleDateFormat オブジェクトを使用したために発生した
例外が報告されています。class NumberFormatException
具体的な理由は、parse メソッドを呼び出すときに
public Date parse(String source) throws ParseException
{
ParsePosition pos = new ParsePosition(0);
Date result = parse(source, pos);
if (pos.index == 0)
throw new ParseException("Unparseable date: \"" + source + "\"" ,
pos.errorIndex);
return result;
}
そして、2 つのパラメーターを指定した上記の parse メソッドの最後の呼び出し
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();
}
}
}
確立メソッドはパラメータ calendar を受け取り、このオブジェクトは SimpleDateFormat オブジェクトのトラバーサルです. 複数のスレッドが同じオブジェクトを使用し、このestablish
メソッドでは合計演算calendar
が実行されます. これにより、マルチスレッド操作下でオブジェクト内のデータが混乱し、データがフォーマットされたときに表示されるべきではない数値が表示されることにもつながります。clear
set
calendar
NumberFormatException
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;
}
フォーマットにも同様の問題があり、例外が発生する場合もあります。
解決
- 1 つ目は、使用されるたびに新しい SimpleDateFormat オブジェクトを直接作成することです。
- 2つ目は、使用時に同期で変更することです。
- 3つ目は
//Lock对象
private static Lock lock = new ReentrantLock();
在simpleDateFormat.parse("2022-12-19");的前面使用lock.lock();然后加上finally语句块,在这个语句快钟进行lock.unlock();
注意这里一定要在finally语句块钟进行执行释放锁的操作,避免因为程序异常导致锁无法是释放的问题。
- 4 番目のオプションは、ThreadLocal を使用して、各スレッドに独自の simpleDateFormat 変数があることを確認し、スレッド セーフの問題が発生しないようにすることです。
//定义成员变量
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
//然后将原来parse的部分该成
threadLocal.get().parse("2022-12-19");
- 5 つ目は、Java8 が提供する新しい定時 API のクラスである DateTimeFormatter クラスを使用することで、高同時実行環境で直接使用できるスレッドセーフなクラスです。
private static DateTimeFormatter dateTimeFormatter =DateTimeFormatter.ofPattern("yyyy-MM-dd");
dateTimeFormatter.parse("2022-12-19");
- 6番目の方法は、.joda-timeメソッドを使用します.この方法は、依存関係を導入する必要があります.ここではあまり紹介しません.上記の方法で十分です.