众所周知,Java中的SimpleDateFormat不是线程安全的,在多线程下会出现意想不到的问题。本文将解析SimpleDateFormat线程不安全的具体缘由,从而加深对线程安全的理解。java
简单的测试代码,当多个线程同时调用parse方法的时候会出问题:git
public class SimpleDateFormatTest { private static SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); public static void main(String[] args) { for (int i = 0; i < 20; i++) { new Thread(() -> { try { System.out.println(format.parse("2019/11/11 11:11:11")); } catch (ParseException e) { e.printStackTrace(); } }).start(); } } }
部分输出以下:编程
Mon Nov 11 11:11:11 GMT 2019 Thu Jan 01 00:00:00 GMT 1970 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:2051) 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 package1.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:17) at package1.SimpleDateFormatTest at java.lang.Thread.run(Thread.java:745) java.lang.NumberFormatException: empty String at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842) 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 package1.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:17) at package1.SimpleDateFormatTest at java.lang.Thread.run(Thread.java:745)
不出意外,每次跑都会报错,偶尔还会出现输出初始时间Thu Jan 01 00:00:00 GMT 1970以及其余莫名其妙的时间。好的,记住这两个错误,下面咱们仔细分析。数组
SimpleDateFormat继承自DateFormat这个抽象类,UML图以下: 缓存
DateFormat中有两个全局变量须要注意安全
public abstract class DateFormat extends Format { //日历变量,做为DateFormat的辅助 protected Calendar calendar; //用来Format数字,默认为DecimalFormat protected NumberFormat numberFormat; } public class DecimalFormat extends NumberFormat { //DecimalFormat中的全局变量,用来存放转化好的数据 //digitList用科学技计数表示,如2019表示成0.2019x10^4 private transient DigitList digitList = new DigitList(); }
这两个变量的初始化在SimpleDateFormat的构造方法里初始化。 看了类结构,咱们仔细分析一下DateFormat的parse方法,直接上代码(省略掉了一些可有可无的代码):多线程
public Date parse(String text, ParsePosition pos) { ...... //注意这个变量calb,日期的转化是经过CalendarBuilder这个类来完成的 CalendarBuilder calb = new CalendarBuilder(); //按照DateFormat的pattern逐个循环(年月日时分秒...) for (int i = 0; i < compiledPattern.length; ) { ...... //最终调用subParse方法给calb赋值 start = subParse(text, start, tag, count, obeyCount, ambiguousYear, pos, useFollowingMinusSignAsDelimiter, calb); } Date parsedDate; try { //调用CalendarBuilder的establish方法,把值传递给变量calendar //经过calendar来获取最终返回的日期 //注意,这里calendar是个全局变量 parsedDate = calb.establish(calendar).getTime(); } ...... return parsedDate; }
主要分为以下几个步骤: > 1. 定义一个CalendarBuilder对象calb,用来临时保存parse结果。 > 2. 根据DateFormat定义的Pattern,for循环调用subParse方法,将目标字符串逐个(年月日时分秒...)转化,并存储在calb变量里。 > 3. 调用calb.establish(calendar)方法,把暂存在calb里的数据设置到全局变量calendar里。 > 4. 如今calendar里已经包含转换过的日期数据,最后调用**Calendar.getTime()**方法返回日期。并发
下面看一下subParse方法里面作了什么,实现上有什么问题。先看代码(省略掉了一些可有可无的代码):ide
public class SimpleDateFormat extends DateFormat { private int subParse(String text, int start, int patternCharIndex, int count, boolean obeyCount, boolean[] ambiguousYear, ParsePosition origPos, boolean useFollowingMinusSignAsDelimiter, CalendarBuilder calb) { //一些变量初始化 ...... //内部调用numberFormat的parse方法,转化数字 //这里的numberFormat就是上面分析过的那个全局变量,默认实例是DecimalFormat //text是代转字符串"2019/11/11 11:11:11", pos是位置,如2019会被转化为0.2019x10^4 number = numberFormat.parse(text, pos); if (number != null) { //转化成int值,如0.2019x10^4会转化成2019 value = number.intValue(); } int index; switch (patternCharIndex) { case PATTERN_YEAR: // 'y' //有年,月,日等等各类case,这里只拿PATTERN_YEAR(年)这种状况举例子 //将numberFormat parse出来的值set到calb里面去 calb.set(field, value); return pos.index; } ...... // 转义失败 origPos.errorIndex = pos.index; return -1; } } //numberFormat.parse(text, pos)方法实现 public class DecimalFormat extends NumberFormat { public Number parse(String text, ParsePosition pos) { //内部调用subparse方法,将text的内容set到digitList上 if (!subparse(text, pos, positivePrefix, negativePrefix, digitList, false, status)) { return null; } ...... //将digitList转变为目标格式 if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) { //parse为Long型 longResult = digitList.getLong(); } else { //parse为double型 doubleResult = digitList.getDouble(); } ..... return gotDouble ? (Number)new Double(doubleResult) : (Number)new Long(longResult); } private final boolean subparse(String text, ParsePosition parsePosition, String positivePrefix, String negativePrefix, DigitList digits, boolean isExponent, boolean status[]) { //一些判断及变量初始化准备 ...... //digitList在这个方法里面叫digits,先对digits先清零处理。 //decimalAt指小数点位置,如0.2019x10^4中decimalAt就是4 //count指数字位数,如0.2019x10^4中count就是4 digits.decimalAt = digits.count = 0; backup = -1; for (; position < text.length(); ++position) { //循环内部对digits一顿猛如虎的赋值操做,设置科学计数法各个部分的变量 //注意这个digits是一个全局变量 ...... } //还要对digits继续操做 if (!sawDecimal) { digits.decimalAt = digitCount; // Not digits.count! } digits.decimalAt += exponent; ...... return true; } }
看到这里,有点并发编程经验的同窗估计就能看出问题了。在subparse这个方法里面不加保护,当多个线程同时对全局变量digits(digitList)进行操做时,这个变量极可能是个无效的值。好比线程A把值设置了一半,另外一个线程B把值又清零初始化了。因而线程A在后面digitList.getDouble()和digitList.getLong()的时候要么获得意料以外的值,要么直接报错NumberFormatException。测试
那么后面的步骤有没有问题呢?继续往下看。 前面说到,方法会先把parse好的值放到CalendarBuilder型的临时变量calb里面,而后调用establish方法,将calb中缓存的值设置到SimpleDateFormat的calendar变量中,下面看看establish方法:
class CalendarBuilder { Calendar establish(Calendar cal) { ...... //这个cal是SimpleDateFormat中的成员变量calendar //先将cal中的数据清除初始化,跟上面digitList同样的套路 cal.clear(); for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) { for (int index = 0; index <= maxFieldIndex; index++) { if (field[index] == stamp) { //前面CalendarBuild暂存的值都放在field数组里, //这里将数组中的值逐个赋给cal cal.set(index, field[MAX_FIELD + index]); break; } } } if (weekDate) { //设置cal的weekdate field cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek); } return cal; } }
仍是一样的问题,因为**calendar(cal)是个全局变量,当多个线程同时调用establish方法的时候,会有线程安全问题。举个简单的例子,线程A原先赋值好了"2019/11/11 11:11:11",结果线程B调用了cal.clear()**将数据又给清掉了,因而线程A回到了解放前,输出了日期"1970/01/01 00:00:00"。
对于线程安全的解决办法,给方法加同步synchronize是最简单的,至关于线程只能一个一个地访问parse方法:
synchronize (this) { System.out.println(format.parse("2019/11/11 11:11:11")); }
固然更common的使用姿式是配合ThreadLocal使用,至关于给每一个线程都定义了一个format变量,线程间互不影响:
private ThreadLocal<simpledateformat> format = new ThreadLocal<simpledateformat>(){ [@Override](https://my.oschina.net/u/1162528) protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); } }; System.out.println(format.get().parse("2019/11/11 11:11:11"));
不过最推荐的仍是,不要用SimpleDateFormat,而是用Java8新引入的类LocalDateTime或者DateTimeFormatter,不只线程安全,并且效率更高。
本文从代码层面分析了SimpleDateFormat线程不安全的缘由。subparse和establish两个方法均可能致使问题,前者还会抛出Exception。 总结下来,问题都是出在全局变量上。因此当咱们定义全局变量的时候必定要谨慎,注意变量是否是线程安全。</simpledateformat></simpledateformat>