SimpleDateFormatのスレッドの不安全問題を解決する方法


Javaプロジェクトでは、次のように日付と文字列の変換を処理するDateUtilクラスを自分で書きます.
public class DateUtil01 {

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

	public void format(Date date) {
		System.out.println(dateformat.format(date));
	}

	public void parse(String str) {
		try {
			System.out.println(dateformat.parse(str));
		} catch (ParseException e) {
			e.printStackTrace();
		}
	}
}

しかし、SimpleDateFormatクラスはスレッドが安全ではないため、マルチスレッド環境では予想外の結果が出ることが多い.以下に、セキュリティ上の問題があるかどうかをテストする例を示します.
1.日付ツール処理クラスのインタフェース
package com.bijian.study.date;

import java.util.Date;

public interface DateUtilInterface {

	public void format(Date date);
	public void parse(String str);
}

 
2.日付ツール実装クラス
package com.bijian.study.date;

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

public class DateUtil01 implements DateUtilInterface {

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

	@Override
	public void format(Date date) {
		System.out.println(dateformat.format(date));
	}

	@Override
	public void parse(String str) {
		try {
			System.out.println(dateformat.parse(str));
		} catch (ParseException e) {
			e.printStackTrace();
		}
	}
}

 
3.日付ツールを呼び出すスレッドクラス
package com.bijian.study.date;

import java.util.Calendar;
import java.util.Date;

public class DateThread implements Runnable {

	DateUtilInterface dateUtil = null;

	public DateThread(DateUtilInterface dateUtil) {
		this.dateUtil = dateUtil;
	}

	public void run() {
		int year = 2000;
		Calendar cal;
		for (int i = 1; i < 100; i++) {
			System.out.println("no." + i);
			year++;
			cal = Calendar.getInstance();
			cal.set(Calendar.YEAR, year);
			//Date date = cal.getTime();
			//dateUtil.format(date);
			dateUtil.parse(year + "-05-25 11:21:21");
			try {
				Thread.sleep(1);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

 
4.主な方法をテストする
package com.bijian.study.date;

public class DateMainTest {

	public static void main(String[] args) {
		
		DateUtilInterface dateUtil = new DateUtil01();
		Runnable runabble = new DateThread(dateUtil);
		for(int i=0;i<10;i++){
            new Thread(runabble).start();
		}
	}
}

 
実行結果:
no.1
no.1
no.1
Fri May 25 11:21:21 CST 2001
Fri May 25 11:21:21 CST 2001
Fri May 25 11:21:21 CST 2001
no.1
no.1
Fri May 25 11:21:21 CST 2001
Fri May 25 11:21:21 CST 2001
no.1
no.1
Fri May 25 11:21:21 CST 2001
no.1
Fri May 25 11:21:21 CST 2001
no.1
Fri May 25 11:00:21 CST 2001
Wed Sep 25 11:21:21 CST 2002
no.1
no.2
no.2
Sat May 25 11:21:21 CST 2002
no.2
no.2
no.2
Sat May 25 11:21:21 CST 2002
no.2
Sat May 25 11:21:21 CST 2002
Fri May 25 11:21:21 CST 2001
Sat May 25 11:21:21 CST 2002
no.2
Sat May 25 11:21:21 CST 2002
no.2
Sat May 25 11:21:21 CST 2002
no.2
Sat May 25 11:21:21 CST 2002
Exception in thread "Thread-2" java.lang.NumberFormatException: For input string: ".00221E.00221E4"
	at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
	at java.lang.Double.parseDouble(Unknown Source)
	at java.text.DigitList.getDouble(Unknown Source)
	at java.text.DecimalFormat.parse(Unknown Source)
	at java.text.SimpleDateFormat.subParse(Unknown Source)
	at java.text.SimpleDateFormat.parse(Unknown Source)
	at java.text.DateFormat.parse(Unknown Source)
	at com.bijian.study.date.DateUtil01.parse(DateUtil01.java:19)
	at com.bijian.study.date.DateThread.run(DateThread.java:24)
	at java.lang.Thread.run(Unknown Source)
Exception in thread "Thread-5" java.lang.NumberFormatException: For input string: ".00221E.00221E44"
	at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
	at java.lang.Double.parseDouble(Unknown Source)
	at java.text.DigitList.getDouble(Unknown Source)
	at java.text.DecimalFormat.parse(Unknown Source)
	at java.text.SimpleDateFormat.subParse(Unknown Source)
	at java.text.SimpleDateFormat.parse(Unknown Source)
	at java.text.DateFormat.parse(Unknown Source)
	at com.bijian.study.date.DateUtil01.parse(DateUtil01.java:19)
	at com.bijian.study.date.DateThread.run(DateThread.java:24)
	at java.lang.Thread.run(Unknown Source)
no.3
no.3
Sun May 25 11:21:21 CST 2003
no.3
no.3
no.3
Sun May 25 11:21:21 CST 2003
no.4
Sun May 25 11:21:21 CST 2003
no.3
Tue May 25 11:21:21 CST 2004
no.2
Sun May 25 11:21:21 CST 2003
no.3
Thu Jan 01 00:21:21 CST 1970
Exception in thread "Thread-7" Exception in thread "Thread-0" java.lang.NumberFormatException: For input string: "E212"
	at java.lang.NumberFormatException.forInputString(Unknown Source)
	at java.lang.Long.parseLong(Unknown Source)
	at java.lang.Long.parseLong(Unknown Source)
	at java.text.DigitList.getLong(Unknown Source)
	at java.text.DecimalFormat.parse(Unknown Source)
	at java.text.SimpleDateFormat.subParse(Unknown Source)
	at java.text.SimpleDateFormat.parse(Unknown Source)
	at java.text.DateFormat.parse(Unknown Source)
	at com.bijian.study.date.DateUtil01.parse(DateUtil01.java:19)
	at com.bijian.study.date.DateThread.run(DateThread.java:24)
	at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: For input string: "E212"
	at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
	at java.lang.Double.parseDouble(Unknown Source)
	at java.text.DigitList.getDouble(Unknown Source)
	at java.text.DecimalFormat.parse(Unknown Source)
	at java.text.SimpleDateFormat.subParse(Unknown Source)
	at java.text.SimpleDateFormat.parse(Unknown Source)
	at java.text.DateFormat.parse(Unknown Source)
	at com.bijian.study.date.DateUtil01.parse(DateUtil01.java:19)
	at com.bijian.study.date.DateThread.run(DateThread.java:24)
	at java.lang.Thread.run(Unknown Source)
Exception in thread "Thread-8" java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(Unknown Source)
	at java.lang.Long.parseLong(Unknown Source)
	at java.lang.Long.parseLong(Unknown Source)
	at java.text.DigitList.getLong(Unknown Source)
	at java.text.DecimalFormat.parse(Unknown Source)
	at java.text.SimpleDateFormat.subParse(Unknown Source)
	at java.text.SimpleDateFormat.parse(Unknown Source)
	at java.text.DateFormat.parse(Unknown Source)
	at com.bijian.study.date.DateUtil01.parse(DateUtil01.java:19)
	at com.bijian.study.date.DateThread.run(DateThread.java:24)
	at java.lang.Thread.run(Unknown Source)
no.4
no.4
...
...
...

 
このような実行結果から,SimpleDateFormatのparseメソッドにはスレッドセキュリティの問題がある.
呼び出し日ツールのスレッドクラスを変更するには、SimpleDateFormatのformatメソッドにスレッドセキュリティの問題があるかどうかをテストします.
package com.bijian.study.date;

import java.util.Calendar;
import java.util.Date;

public class DateThread implements Runnable {

	DateUtilInterface dateUtil = null;

	public DateThread(DateUtilInterface dateUtil) {
		this.dateUtil = dateUtil;
	}

	public void run() {
		int year = 2000;
		Calendar cal;
		for (int i = 1; i < 100; i++) {
			System.out.println("no." + i);
			year++;
			cal = Calendar.getInstance();
			cal.set(Calendar.YEAR, year);
			Date date = cal.getTime();
			dateUtil.format(date);
			//dateUtil.parse(year + "-05-25 11:21:21");
			try {
				Thread.sleep(1);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

実行結果:
no.1
no.1
2001-05-22 13:07:22
2001-05-22 13:07:22
no.1
no.1
2001-05-22 13:07:22
2001-05-22 13:07:22
no.1
2001-05-22 13:07:22
no.1
no.1
2001-05-22 13:07:22
2001-05-22 13:07:22
no.1
no.1
2001-05-22 13:07:22
2001-05-22 13:07:22
no.1
2001-05-22 13:07:22
no.2
no.2
no.2
no.2
2002-05-22 13:07:22
no.2
2002-05-22 13:07:22
2002-05-22 13:07:22
no.2
2002-05-22 13:07:22
no.2
...
...
...

複数回実行しても異常は発生しないため、個人予測ではSimpleDateFormatのformatメソッドにはスレッドセキュリティの問題はない. 
      
以上の安全問題を解決するには、3つの方法があります.  1).同期の使用
package com.bijian.study.date;

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

public class DateUtil02 implements DateUtilInterface {

	private SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	
	@Override
	public void format(Date date) {
		System.out.println(dateformat.format(date));
	}

	@Override
	public void parse(String str) {
		try {
			synchronized(dateformat){
				System.out.println(dateformat.parse(str));
			}
		} catch (ParseException e) {
			e.printStackTrace();
		}
	}
}

DateMainTestを修正します.JAvaのDateUtilInterface dateUtil=new DateUtil 01();DateUtilInterface dateUtil=new DateUtil 02();テストOK.
ただし、スレッドが多い場合、1つのスレッドがメソッドを呼び出すと、他のメソッドを呼び出すスレッドがブロックされ、パフォーマンスにある程度影響します.
 
  2).使用するたびに、新しいSimpleDateFormatインスタンスが作成されます.
package com.bijian.study.date;

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

public class DateUtil03 implements DateUtilInterface {

	@Override
	public void format(Date date) {
		
		SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		System.out.println(dateformat.format(date));
	}

	@Override
	public void parse(String str) {
		try {
			SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			System.out.println(dateformat.parse(str));
		} catch (ParseException e) {
			e.printStackTrace();
		}
	}
}

DateMainTestを修正します.JAvaのDateUtilInterface dateUtil=new DateUtil 02();DateUtilInterface dateUtil=new DateUtil 03()テストOK.
  
  3).ThreadLocalオブジェクトを使用してスレッドごとに1つのインスタンスのみを作成
ThreadLocalオブジェクトを使用してスレッドごとにインスタンスを1つだけ作成することが推奨されます.
各スレッドのSimpleDateFormatには、コラボレーションに影響を与える状態はありません.各スレッドに対してSimpleDateFormat変数のコピーまたはコピーを作成します.コードは次のとおりです.
package com.bijian.study.date;

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

public class DateUtil04 implements DateUtilInterface {

	private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

	//      get   null
	private static ThreadLocal threadLocal = new ThreadLocal(){
		protected Object initialValue() {  
			return null;//    null  
		} 
	};
	
	//          ,     initialValue,   get  null,        SimpleDateFormat, set threadLocal 
	public static DateFormat getDateFormat() {
		DateFormat df = (DateFormat) threadLocal.get();
		if (df == null) {
			df = new SimpleDateFormat(DATE_FORMAT);
			threadLocal.set(df);
		}
		return df;
	}

	@Override
	public void parse(String textDate) {

		try {
			System.out.println(getDateFormat().parse(textDate));
		} catch (ParseException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void format(Date date) {
		System.out.println(getDateFormat().format(date));
	}
}

ThreadLocalクラス変数を作成します.ここでは、作成時に匿名クラスを使用し、initialValueメソッドを上書きします.主な役割は、作成時にインスタンスを初期化することです.
以下の方法で作成することもできます.
package com.bijian.study.date;

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

public class DateUtil05 implements DateUtilInterface {

	private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
	
	@SuppressWarnings("rawtypes")
	private static ThreadLocal threadLocal = new ThreadLocal() {
		protected synchronized Object initialValue() {
			return new SimpleDateFormat(DATE_FORMAT);
		}
	};

	public DateFormat getDateFormat() {
		return (DateFormat) threadLocal.get();
	}

	@Override
	public void parse(String textDate) {

		try {
			System.out.println(getDateFormat().parse(textDate));
		} catch (ParseException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void format(Date date) {
		System.out.println(getDateFormat().format(date));
	}
}

DateMainTestを修正します.JAvaのDateUtilInterface dateUtil=new DateUtil 03();DateUtilInterface dateUtil=new DateUtil 04();またはDateUtilInterface dateUtil=new DateUtil 05();テストOK.
 
最後に、apache commons-langパッケージのDateFormatUtilsまたはFastDateFormatを使用して実装することもできます.apacheはスレッドが安全で、より効率的であることを保証します.
 
添付:Oracle公式バグの説明:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6178997