SimpleDateFormat 线程安全问题
为啥线程不安全?
主要包含两大块 parse 和 format 不安全。
可以看到,多个线程之间共享变量calendar,并修改calendar。因此在多线程环境下,当多个线程同时使用相同的SimpleDateFormat对象(如static修饰)的话,如调用format方法时,多个线程会同时调用calender.setTime方法,导致time被别的线程修改,因此线程是不安全的。
SimpleDateFormat 的 format 方法线程不安全问题
public final String format(Date arg0) {
return this.format(arg0, new StringBuffer(),
DontCareFieldPosition.INSTANCE).toString();
}
this.format 使用的是 SimpleDateFormat 的format 方法
private StringBuffer format(Date arg0, StringBuffer arg1, FieldDelegate arg2) {
this.calendar.setTime(arg0);
boolean arg3 = this.useDateFormatSymbols();
int arg4 = 0;
while (arg4 < this.compiledPattern.length) {
int arg5 = this.compiledPattern[arg4] >>> 8;
int arg6 = this.compiledPattern[arg4++] & 255;
if (arg6 == 255) {
arg6 = this.compiledPattern[arg4++] << 16;
arg6 |= this.compiledPattern[arg4++];
}
switch (arg5) {
case 100 :
arg1.append((char) arg6);
break;
case 101 :
arg1.append(this.compiledPattern, arg4, arg6);
arg4 += arg6;
break;
default :
this.subFormat(arg5, arg6, arg2, arg1, arg3);
}
}
return arg1;
}
不同线程
this.calendar.setTime(arg0);
依然会导致线程不安全问题。
SimpleDateFormat是继承DateFormat类,DateFormat类中维护一个Calendar 对象
SimpleDateFormat 继承 DateFormat ,使用的calendar 是父类 DateFormat中的
public class SimpleDateFormat extends DateFormat {}
DateFormat 的 calendar 被用来进行 日期-时间计算,也被用于 format 方法也被用于 parse方法
public abstract class DateFormat extends Format {
protected Calendar calendar;
}
Parse 导致的线程安全问题
SimpleDateFormat 的 parse 方法
public Date parse(String arg0, ParsePosition arg1) {
this.checkNegativeNumberExpression();
int arg2 = arg1.index;
int arg3 = arg2;
int arg4 = arg0.length();
boolean[] arg5 = new boolean[]{false};
CalendarBuilder arg6 = new CalendarBuilder();
int arg7 = 0;
label82 : while (arg7 < this.compiledPattern.length) {
int arg8 = this.compiledPattern[arg7] >>> 8;
int arg9 = this.compiledPattern[arg7++] & 255;
if (arg9 == 255) {
arg9 = this.compiledPattern[arg7++] << 16;
arg9 |= this.compiledPattern[arg7++];
}
switch (arg8) {
case 100 :
if (arg2 < arg4 && arg0.charAt(arg2) == (char) arg9) {
++arg2;
break;
}
arg1.index = arg3;
arg1.errorIndex = arg2;
return null;
case 101 :
while (true) {
if (arg9-- <= 0) {
continue label82;
}
if (arg2 >= arg4
|| arg0.charAt(arg2) != this.compiledPattern[arg7++]) {
arg1.index = arg3;
arg1.errorIndex = arg2;
return null;
}
++arg2;
}
default :
boolean arg10 = false;
boolean arg11 = false;
if (arg7 < this.compiledPattern.length) {
int arg12 = this.compiledPattern[arg7] >>> 8;
if (arg12 != 100 && arg12 != 101) {
arg10 = true;
}
if (this.hasFollowingMinusSign
&& (arg12 == 100 || arg12 == 101)) {
int arg13;
if (arg12 == 100) {
arg13 = this.compiledPattern[arg7] & 255;
} else {
arg13 = this.compiledPattern[arg7 + 1];
}
if (arg13 == this.minusSign) {
arg11 = true;
}
}
}
arg2 = this.subParse(arg0, arg2, arg8, arg9, arg10, arg5,
arg1, arg11, arg6);
if (arg2 < 0) {
arg1.index = arg3;
return null;
}
}
}
arg1.index = arg2;
try {
Date arg15 = arg6.establish(this.calendar).getTime();
if (arg5[0] && arg15.before(this.defaultCenturyStart)) {
arg15 = arg6.addYear(100).establish(this.calendar).getTime();
}
return arg15;
} catch (IllegalArgumentException arg14) {
arg1.errorIndex = arg2;
arg1.index = arg3;
return null;
}
}
关键看
Date arg15 = arg6.establish(this.calendar).getTime();
这个里面 的 establish 方法。establish 是 CalendarBuilder 的方法
Calendar establish(Calendar arg0) {
boolean arg1 = this.isSet(17) && this.field[17] > this.field[1];
if (arg1 && !arg0.isWeekDateSupported()) {
if (!this.isSet(1)) {
this.set(1, this.field[35]);
}
arg1 = false;
}
arg0.clear();
int arg2;
int arg3;
for (arg2 = 2; arg2 < this.nextStamp; ++arg2) {
for (arg3 = 0; arg3 <= this.maxFieldIndex; ++arg3) {
if (this.field[arg3] == arg2) {
arg0.set(arg3, this.field[18 + arg3]);
break;
}
}
}
if (arg1) {
arg2 = this.isSet(3) ? this.field[21] : 1;
arg3 = this.isSet(7) ? this.field[25] : arg0.getFirstDayOfWeek();
if (!isValidDayOfWeek(arg3) && arg0.isLenient()) {
if (arg3 >= 8) {
--arg3;
arg2 += arg3 / 7;
arg3 = arg3 % 7 + 1;
} else {
while (arg3 <= 0) {
arg3 += 7;
--arg2;
}
}
arg3 = toCalendarDayOfWeek(arg3);
}
arg0.setWeekDate(this.field[35], arg2, arg3);
}
return arg0;
}
主要看
arg0.clear();
这个会将 calendar 清除掉,并且没有设置新值
可知SimpleDateFormat维护的用于format和parse方法计算日期-时间的calendar被清空了,如果此时线程A将calendar清空且没有来得及设置新值,线程B也进入parse方法用到了SimpleDateFormat对象中的calendar对象,此时就会产生线程安全问题!
解决方案:
1、将SimpleDateFormat定义成局部变量
2、 加一把线程同步锁:synchronized(lock)
3、使用ThreadLocal,每个线程都拥有自己的SimpleDateFormat对象副本
解决办法栗子:threadLocal
class ThreadLocalSimpleFormatDateUtil {
private static final String date_format = "yyyy-MM-dd HH:mm:ss";
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();
public static DateFormat getDateFormat() {
DateFormat df = threadLocal.get();
if (df == null) {
df = new SimpleDateFormat(date_format);
threadLocal.set(df);
}
return df;
}
public static String formatDate(Date date) throws ParseException {
return getDateFormat().format(date);
}
public static Date parse(String strDate) throws ParseException {
return getDateFormat().parse(strDate);
}
}
测试代码
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
class MySimpleDateFormatThread extends Thread {
private SimpleDateFormat sdf;
private String dateString;
public MySimpleDateFormatThread(SimpleDateFormat sdf, String dateString) {
this.sdf = sdf;
this.dateString = dateString;
}
@Override
public void run() {
try {
Date date = sdf.parse(dateString);
String dateStr = sdf.format(date);
if(!dateStr.equals(dateString)) {
System.out.println("ThreadName=" + this.getName() + "报错了,日期字符串:" + dateString + ",转换成的日期字符串:" + dateStr);
} else {
System.out.println("ThreadName=" + this.getName() + "成功,日期字符串:" + dateString);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class TestSimpleDateFormat {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String[] dateString = {"2017-11-05","2017-11-06","2017-11-07","2017-11-08","2017-11-09","2017-11-10","2017-11-11","2017-11-12","2017-11-13","2017-11-14"};
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new MySimpleDateFormatThread(sdf, dateString[i]);
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
}
}
会有如下报错:
ThreadName=Thread-3报错了,日期字符串:2017-11-08,转换成的日期字符串:2016-12-08
ThreadName=Thread-2报错了,日期字符串:2017-11-07,转换成的日期字符串:2016-12-08
ThreadName=Thread-7报错了,日期字符串:2017-11-12,转换成的日期字符串:2016-12-08
ThreadName=Thread-4报错了,日期字符串:2017-11-09,转换成的日期字符串:2200-11-09
ThreadName=Thread-8成功,日期字符串:2017-11-13java.lang.NumberFormatException: multiple points
ThreadName=Thread-9成功,日期字符串:2017-11-14
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(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 JavaThread.MySimpleDateFormatThread.run(TestSimpleDateFormat.java:34)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(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 JavaThread.MySimpleDateFormatThread.run(TestSimpleDateFormat.java:34)
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 JavaThread.MySimpleDateFormatThread.run(TestSimpleDateFormat.java:34)
java.lang.ArrayIndexOutOfBoundsException: -1
at java.text.DigitList.fitsIntoLong(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 JavaThread.MySimpleDateFormatThread.run(TestSimpleDateFormat.java:34)
https://mp.weixin.qq.com/s/nAHdbkWH6qY9D2byrZHf3A