`

解决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
...
...
...

 

      从如上运行结果来看,SimpleDateFormatparse方法有线程安全问题。

   修改调用日期工具的线程类如下,测试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方法没有线程安全的问题。 

      

        有三种方法可以解决以上安全问题。
  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 DateUtil01();为DateUtilInterface dateUtil = new DateUtil02();测试OK。

        不过,当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,这样的操作也会一定程度上影响性能。

 

  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 DateUtil02();为DateUtilInterface dateUtil = new DateUtil03();测试OK。

  

  3).借助ThreadLocal对象每个线程只创建一个实例

     借助ThreadLocal对象每个线程只创建一个实例,这是推荐的方法

     对于每个线程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 DateUtil03();为DateUtilInterface dateUtil = new DateUtil04();或者DateUtilInterface dateUtil = new DateUtil05();测试OK。

 

      最后,当然也可以使用apache commons-lang包的DateFormatUtils或者FastDateFormat实现,apache保证是线程安全的,并且更高效。

 

     附:Oracle官方bug说明: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6178997

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics