SimpleDateFormat是Java中非常常见的一个类,用来解析和格式化日期字符串。但是SimpleDateFormat在多线程的环境并不是安全的,这个是很容易犯错的部分,接下来讲一下这个问题出现的过程以及解决的思路。
问题描述:
先看代码,用来获取一个月的天数的:
importjava.text.SimpleDateFormat;importjava.util.Calendar;importjava.util.Date;public classDateUtil {/*** 获取月份天数
*@paramtime 201202
*@return
*/
public static int getDays(String time) throwsException {//String time = "201202";
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
Date date=sdf.parse(time);
Calendar c=Calendar.getInstance();
c.setTime(date);int day =c.getActualMaximum(Calendar.DATE);returnday;
}
}
可以看到在这个方法里,每次要获取值的时候就先要创建一个SimpleDateFormat的实例,频繁调用这个方法的情况下很耗性能。为了避免大量实例的频繁创建和销毁,我们通常会使用单例模式或者静态变量进行改造,一般会这么改:
importjava.text.SimpleDateFormat;importjava.util.Calendar;importjava.util.Date;public classDateUtil {private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");/*** 获取月份天数
*@paramtime 201202
*@return
*/
public static int getDays(String time) throwsException {//String time = "201202";
Date date =sdf.parse(time);
Calendar c=Calendar.getInstance();
c.setTime(date);int day =c.getActualMaximum(Calendar.DATE);returnday;
}
}
此时不管调用多少次这个方法,java虚拟机里只有一个SimpleDateFormat对象,效率和性能肯定要比第一个方法好,这个也是很多程序员选择的方法。但是,在这个多线程的条件下,多个thread共享同一个SimpleDateFormat,而SimpleDateFormat本身又是线程非安全的,这样就很容易出各种问题。
验证问题:
用一个简单的例子验证一下多线程环境下SimpleDateFormat的运行结果:
importjava.text.ParseException;importjava.text.SimpleDateFormat;importjava.util.Date;importjava.util.concurrent.CountDownLatch;public classDateUtil {private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public staticString format(Date date) {returndateFormat.format(date);
}public static Date parse(String dateStr) throwsParseException {returndateFormat.parse(dateStr);
}public static voidmain(String[] args) {final CountDownLatch latch = new CountDownLatch(1);final String[] strs = new String[] {"2016-01-01 10:24:00", "2016-01-02 20:48:00", "2016-01-11 12:24:00"};for (int i = 0; i < 10; i++) {new Thread(newRunnable() {
@Overridepublic voidrun() {try{
latch.await();
}catch(InterruptedException e) {
e.printStackTrace();
}for (int i = 0; i < 10; i++){try{
System.out.println(Thread.currentThread().getName()+ "\t" + parse(strs[i %strs.length]));
Thread.sleep(100);
}catch(ParseException e) {
e.printStackTrace();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
latch.countDown();
}
}
看一下运行的结果:
Thread-9 Fri Jan 01 10:24:00 CST 2016Thread-1 Sat Feb 25 00:48:00 CST 20162017Thread-5 Sat Feb 25 00:48:00 CST 20162017Exception in thread"Thread-4" Exception in thread "Thread-6" java.lang.NumberFormatException: For input string: "2002.E20022E"at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
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 DateUtil.parse(DateUtil.java:24)
at DateUtil$2.run(DateUtil.java:45)
那么为什么SimpleDateFormat不是线程安全的呢?
查找问题:
首先看一下SimpleDateFormat的源码:
privateStringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {//Convert input date to time field list
calendar.setTime(date);boolean useDateFormatSymbols =useDateFormatSymbols();for (int i = 0; i >> 8;int count = compiledPattern[i++] & 0xff;if (count == 255) {
count= compiledPattern[i++] << 16;
count|= compiledPattern[i++];
}switch(tag) {caseTAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);break;caseTAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i+=count;break;default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);break;
}
}returntoAppendTo;
}
可以看到format()方法先将日期存放到一个Calendar对象中,而这个Calendar在SimpleDateFormat中是以成员变量的形式存在的。随后调用subFormat()时会再次用到成员变量Calendar。这就是问题所在。同样,在parse()方法里也会存在相应的问题。
试想,在多线程环境下,如果两个线程都使用同一个SimpleDateFormat实例,那么就有可能存在其中一个线程修改了calendar后紧接着另一个线程也修改了calendar,那么随后第一个线程用到calendar时已经不是它所期待的值了。
避免问题:
那么,如何保证SimpleDateFormat的线程安全呢?
1.每次使用SimpleDateFormat时都创建一个局部的SimpleDateFormat对象,跟一开始的那个方法一样,但是存在性能上的问题,开销较大。
2.加锁或者同步
importjava.text.ParseException;importjava.text.SimpleDateFormat;importjava.util.Date;public classDateSyncUtil {private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static String formatDate(Date date)throwsParseException{synchronized(sdf){returnsdf.format(date);
}
}public static Date parse(String strDate) throwsParseException{synchronized(sdf){returnsdf.parse(strDate);
}
}
}
当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。
3.使用ThreadLocal
public classDateUtil {private static ThreadLocal local = new ThreadLocal() {
@OverrideprotectedSimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};public staticString format(Date date) {returnlocal.get().format(date);
}public static Date parse(String dateStr) throwsParseException {returnlocal.get().parse(dateStr);
}
}
使用ThreadLocal可以确保每个线程都可以得到一个单独的SimpleDateFormat对象,既避免了频繁创建对象,也避免了多线程的竞争。