SimpleDateFormat线程安全问题详解
在平时的工作中,我们经常需要将日期在String和Date之间做转化,此时需要使用SimpleDateFormat类。使用SimpleDateFormat类的parse方法,可以将满足格式要求的字符串转换成Date对象;使用SimpleDateFormat类的format方法,可以将Date类型的对象转换成一定格式的字符串!但是有一点需要特别注意,SimpleDateFormat并非是线程安全的,也就是说在并发环境下,如果考虑不周使用SimpleDateFormat方法可以会出现线程安全方面的问题
一、线程安全问题
1.示例
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Created on 2018/11/03.
*/
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date)throws ParseException {
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException{
return sdf.parse(strDate);
}
}
import java.text.ParseException;
/**
* Created on 2018/11/03.
*/
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();
}
}
}
2.运行结果:
Exception in thread "Thread-4" Exception in thread "Thread-2" Exception in thread "Thread-0" java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at javaStudy.DateUtil.parse(DateUtil.java:19)
at javaStudy.DateUtilTest$SimpleDateFormatThread.run(DateUtilTest.java:21)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
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 javaStudy.DateUtil.parse(DateUtil.java:19)
at javaStudy.DateUtilTest$SimpleDateFormatThread.run(DateUtilTest.java:21)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
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 javaStudy.DateUtil.parse(DateUtil.java:19)
at javaStudy.DateUtilTest$SimpleDateFormatThread.run(DateUtilTest.java:21)
Thread-3:Fri Nov 03 01:19:07 CST 2220
Thread-1:Fri Mar 03 02:02:47 CST 2220
3.对运行结果进行分析
从结果中我们可以看到:总共运行5个线程(除了主线程main之外),其中:Thread-4、Thread-2和Thread-0都出现NumberFormatException;Thread-3和Thread-1虽然没有出现异常,但是我们输入的是:2018-11-03 10:02:47,但是输出的却是2220年的时间
二、为什么会出现线程不安全
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方法为什么线程不安全
parse()调用情况:
1)先调用DateFormat对象的public Date parse(String source) throws ParseException
2)DateFormat对象的parse方法调用SimpleDateFormat对象的public Date parse(String text, ParsePosition pos)
3)SimpleDateFormat对象的parse方法调用 CalendarBuilder 对象的 Calendar establish(Calendar cal)
4)在 establish()中,做了cal.clear();把calendar清空且没有设置新值。如果此时线程A将calendar清空且没有设置新值,线程B也进入parse方法用到了SimpleDateFormat对象中的calendar对象,此时就会产生线程安全问题!
2.format为什么线程不安全
1)DateFormat类中
public final String format(Date date)
{
return format(date, new StringBuffer(),
DontCareFieldPosition.INSTANCE).toString();
}
2)SimpleDateFormat类中
public StringBuffer format(Date date, StringBuffer toAppendTo,
FieldPosition pos)
{
pos.beginIndex = pos.endIndex = 0;
return format(date, toAppendTo, pos.getFieldDelegate());
}
// Called from Format after creating a FieldDelegate
private StringBuffer 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 < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
calendar.setTime(date)这条语句改变了calendar。假设两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
线程1调用format方法,改变了calendar这个字段。
中断来了。
线程2开始执行,它也改变了calendar。
又中断了。
线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。
3.总结
从parse() 和format()可以看出:成员变量calendar在用到的时候直接用,且在用的时候改变了calendar的值,这样如果多线程情况下,就会出现线程安全问题。实际上可以:在最初调用的时候定义一个局部变量calendar,一路通过参数传递下去,所有问题都将迎刃而解。
三、解决方案
1.需要的时候创建局部变量
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
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实例变量,但在使用的时候,需要对这个变量进行同步
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
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);
}
}
}
当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。
3.使用ThreadLocal为每个线程都创建一个线程独享的SimpleDateFormat变量
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
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, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。