想必大家对SimpleDateFormat并不陌生。SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调试的问题,因为 DateFormat 和 SimpleDateFormat 类不都是线程安全的,在多线程环境下调用 format() 和 parse() 方法会出现问题。下面我们通过一个具体的场景来一步步的深入学习和理解SimpleDateFormat类。
示例
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String format(Date date) throws ParseException {
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException {
return sdf.parse(strDate);
}
}
public class DateUtilTest {
public static class SimpleDateFormatThread extends Thread {
private String dateStr;
public SimpleDateFormatThread(String dateStr) {
this.dateStr = dateStr;
}
@Override
public void run() {
try {
System.out.println(this.getName() + ":" + DateUtil.parse(dateStr));
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
String dateStr = "2018-11-03 10:02:47";
for (int i = 0; i < 5; i++) {
new SimpleDateFormatThread(dateStr).start();
}
}
}
运行结果:
Exception in thread "Thread-3" Exception in thread "Thread-2" Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.kang.test.DateUtil.parse(DateUtil.java:16)
at com.kang.test.DateUtilTest$SimpleDateFormatThread.run(DateUtilTest.java:19)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.kang.test.DateUtil.parse(DateUtil.java:16)
at com.kang.test.DateUtilTest$SimpleDateFormatThread.run(DateUtilTest.java:19)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.kang.test.DateUtil.parse(DateUtil.java:16)
at com.kang.test.DateUtilTest$SimpleDateFormatThread.run(DateUtilTest.java:19)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.kang.test.DateUtil.parse(DateUtil.java:16)
at com.kang.test.DateUtilTest$SimpleDateFormatThread.run(DateUtilTest.java:19)
Thread-4:Sat Nov 03 10:02:47 CST 2018
为什么会出现线程不安全
public class SimpleDateFormat extends DateFormat {
// the official serial version ID which says cryptically
// which version we're compatible with
static final long serialVersionUID = 4774881970558875024L;
// the internal serial version which says which version was written
// - 0 (default) for version up to JDK 1.1.3
// - 1 for version from JDK 1.1.4, which includes a new field
static final int currentSerialVersion = 1;
}
public abstract class DateFormat extends Format {
/**
* The {@link Calendar} instance used for calculating the date-time fields
* and the instant of time. This field is used for both formatting and
* parsing.
*
* <p>Subclasses should initialize this field to a {@link Calendar}
* appropriate for the {@link Locale} associated with this
* <code>DateFormat</code>.
* @serial
*/
protected Calendar calendar;
}
通过上面的代码可以看出:SimpleDateFormat继承了DateFormat,在DateFormat中定义了一个protected属性的Calendar类型的成员变量:calendar。在SimpleDateFormat转换日期是通过calendar来操作的,且calendar既被用于format方法也被用于parse方法。
1、parse(String source)方法为什么线程不安全
parse(String source)调用情况:
- 先调用DateFormat对象的 public Date parse(String source)
- DateFormat对象的parse方法调用SimpleDateFormat对象的 public Date parse(String text, ParsePosition pos)
- SimpleDateFormat对象的parse方法调用 CalendarBuilder 对象的 Calendar establish(Calendar cal)
- 在 establish(Calendar cal)做了cal.clear()操作,把calendar清空且没有设置新值。
如果此时线程A将calendar清空且没有设置新值,线程B也进入parse方法用到了SimpleDateFormat对象中的calendar对象,此时就会产生线程安全问题!
2、format(Date date)为什么线程不安全
format(Date date)调用情况:
- 先调用DateFormat对象的 public final String format(Date date)
- DateFormat对象的format方法调用SimpleDateFormat对象的 public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos)
- SimpleDateFormat对象的format方法调用SimpleDateFormat对象的 private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate)
- SimpleDateFormat对象的format方法调用 Calendar 对象的 setTime(Date date)
calendar.setTime(date)这条语句改变了calendar。假设两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
线程1调用format方法,改变了calendar这个字段。中断来了,线程2开始执行,它也改变了calendar,又中断了。线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。
三、解决方案
1、需要的时候创建局部变量
public class DateUtil1 {
public static String formatDate(Date date)throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(strDate);
}
}
在需要用到SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响并不是很明显的。
2、创建一个共享的SimpleDateFormat实例变量,但在使用的时候对这个变量进行同步
public class DateUtil2 {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date)throws ParseException {
synchronized (sdf){
return sdf.format(date);
}
}
public static Date parse(String strDate) throws ParseException{
synchronized (sdf){
return sdf.parse(strDate);
}
}
}
当一个线程调用该方法时,其他想要调用此方法的线程就要阻塞,多线程并发量大的时候会对性能有一定的影响。
3、使用ThreadLocal为每个线程都创建一个线程独享的SimpleDateFormat变量
public class DateUtil3 {
private static ThreadLocal<DateFormat> sdfThreadLocal = new ThreadLocal<DateFormat>(){
@Override
public SimpleDateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String formatDate(Date date)throws ParseException {
return sdfThreadLocal.get().format(date);
}
public static Date parse(String strDate) throws ParseException{
return sdfThreadLocal.get().parse(strDate);
}
}
使用ThreadLocal,也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。