为啥线程不安全?
主要包含两大块 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 = ;
while (arg4 < this.compiledPattern.length) {
int arg5 = this.compiledPattern[arg4] >>> ;
int arg6 = this.compiledPattern[arg4++] & ;
if (arg6 == ) {
arg6 = this.compiledPattern[arg4++] << ;
arg6 |= this.compiledPattern[arg4++];
}
switch (arg5) {
case :
arg1.append((char) arg6);
break;
case :
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 = ;
label82 : while (arg7 < this.compiledPattern.length) {
int arg8 = this.compiledPattern[arg7] >>> ;
int arg9 = this.compiledPattern[arg7++] & ;
if (arg9 == ) {
arg9 = this.compiledPattern[arg7++] << ;
arg9 |= this.compiledPattern[arg7++];
}
switch (arg8) {
case :
if (arg2 < arg4 && arg0.charAt(arg2) == (char) arg9) {
++arg2;
break;
}
arg1.index = arg3;
arg1.errorIndex = arg2;
return null;
case :
while (true) {
if (arg9-- <= ) {
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] >>> ;
if (arg12 != && arg12 != ) {
arg10 = true;
}
if (this.hasFollowingMinusSign
&& (arg12 == || arg12 == )) {
int arg13;
if (arg12 == ) {
arg13 = this.compiledPattern[arg7] & ;
} else {
arg13 = this.compiledPattern[arg7 + ];
}
if (arg13 == this.minusSign) {
arg11 = true;
}
}
}
arg2 = this.subParse(arg0, arg2, arg8, arg9, arg10, arg5,
arg1, arg11, arg6);
if (arg2 < ) {
arg1.index = arg3;
return null;
}
}
}
arg1.index = arg2;
try {
Date arg15 = arg6.establish(this.calendar).getTime();
if (arg5[] && arg15.before(this.defaultCenturyStart)) {
arg15 = arg6.addYear().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() && this.field[] > this.field[];
if (arg1 && !arg0.isWeekDateSupported()) {
if (!this.isSet()) {
this.set(, this.field[]);
}
arg1 = false;
}
arg0.clear();
int arg2;
int arg3;
for (arg2 = ; arg2 < this.nextStamp; ++arg2) {
for (arg3 = ; arg3 <= this.maxFieldIndex; ++arg3) {
if (this.field[arg3] == arg2) {
arg0.set(arg3, this.field[ + arg3]);
break;
}
}
}
if (arg1) {
arg2 = this.isSet() ? this.field[] : ;
arg3 = this.isSet() ? this.field[] : arg0.getFirstDayOfWeek();
if (!isValidDayOfWeek(arg3) && arg0.isLenient()) {
if (arg3 >= ) {
--arg3;
arg2 += arg3 / ;
arg3 = arg3 % + ;
} else {
while (arg3 <= ) {
arg3 += ;
--arg2;
}
}
arg3 = toCalendarDayOfWeek(arg3);
}
arg0.setWeekDate(this.field[], 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[];
for (int i = ; i < threads.length; i++) {
threads[i] = new MySimpleDateFormatThread(sdf, dateString[i]);
}
for (int i = ; i < threads.length; i++) {
threads[i].start();
}
}
}
会有如下报错:
ThreadName=Thread-报错了,日期字符串:--,转换成的日期字符串:--
ThreadName=Thread-报错了,日期字符串:--,转换成的日期字符串:--
ThreadName=Thread-报错了,日期字符串:--,转换成的日期字符串:--
ThreadName=Thread-报错了,日期字符串:--,转换成的日期字符串:--
ThreadName=Thread-成功,日期字符串:--java.lang.NumberFormatException: multiple points
ThreadName=Thread-成功,日期字符串:--
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:)
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:)
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:)
java.lang.ArrayIndexOutOfBoundsException: -
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:)