在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方法没有线程安全的问题。
有三种方法可以解决以上安全问题。
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