DataUtils工具类优化

前言

之前就看资料说SimpleDateFormat是线程不安全的,正好框架里面有自己的DataUtils类,正好优化并学习一下

环境

jdk1.7

测试框架中代码

开20个线程去格式日期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(100);
// 开启20个线程
for (int i = 0; i < 20; i++) {
service.execute(new Runnable() {

@Override
public void run() {
for (int j = 0; j < 10; j++) {
System.out.println(DateUtils.parseDate("2019-03-22 10:03:25"));
}
}
});
}

service.shutdown();
service.awaitTermination(1, TimeUnit.DAYS);

}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Sun Mar 01 10:03:25 CST 1970
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
java.lang.NumberFormatException: For input string: "33E"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Long.parseLong(Long.java:419)
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
at java.lang.Long.parseLong(Long.java:468)Thu Jan 01 00:03:25 CST 1970

at java.text.DigitList.getLong(DigitList.java:177)
at java.text.DecimalFormat.parse(DecimalFormat.java:1297)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934)
Fri Mar 22 10:03:25 CST 2019
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
Fri Mar 22 10:03:25 CST 2019
at java.text.DateFormat.parse(DateFormat.java:335)
at com.resoft.original.framework.common.OriginalDateUtils.parseDate(OriginalDateUtils.java:275)
Fri Mar 22 10:03:25 CST 2019
at com.resoft.original.framework.common.Test$1.run(Test.java:35)
Fri Mar 22 10:03:25 CST 2019 at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
Fri Mar 22 10:03:25 CST 2019 at java.lang.Thread.run(Thread.java:662)

null
java.lang.NumberFormatException: For input string: ""Tue Jan 01 00:03:25 CST 2019

at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)Thu Jan 22 10:03:25 CST 1970

at java.lang.Long.parseLong(Long.java:431)
Wed Dec 31 00:00:00 CST 1969
at java.lang.Long.parseLong(Long.java:468)
at java.text.DigitList.getLong(DigitList.java:177)
Tue Jan 01 00:03:25 CST 2019
at java.text.DecimalFormat.parse(DecimalFormat.java:1297)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
Tue Jan 01 00:03:25 CST 2019
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)Thu Jan 22 00:00:25 CST 1970

at com.resoft.original.framework.common.OriginalDateUtils.parseDate(OriginalDateUtils.java:275)
at com.resoft.original.framework.common.Test$1.run(Test.java:35)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
Tue Jan 01 00:00:25 CST 2019 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)

Thu Jan 01 00:44:40 CST 1970
at java.lang.Thread.run(Thread.java:662)
Tue Jan 01 03:03:25 CST 2019
nulljava.lang.NumberFormatException: For input string: ""

at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Long.parseLong(Long.java:431)
Thu Jan 01 00:00:25 CST 1970
at java.lang.Long.parseLong(Long.java:468)
at java.text.DigitList.getLong(DigitList.java:177)
at java.text.DecimalFormat.parse(DecimalFormat.java:1297)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
Fri Mar 22 10:03:25 CST 2019
at com.resoft.original.framework.common.OriginalDateUtils.parseDate(OriginalDateUtils.java:275)
at com.resoft.original.framework.common.Test$1.run(Test.java:35)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
at java.lang.Thread.run(Thread.java:662)
java.lang.NumberFormatException: multiple pointsnull

Fri Mar 22 10:03:25 CST 2019
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
at java.lang.Double.parseDouble(Double.java:510)
Fri Mar 22 10:03:25 CST 2019
at java.text.DigitList.getDouble(DigitList.java:151)
at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
Thu Jul 10 10:03:25 CST 5881580
at java.text.DateFormat.parse(DateFormat.java:335)
at com.resoft.original.framework.common.OriginalDateUtils.parseDate(OriginalDateUtils.java:275)
at com.resoft.original.framework.common.Test$1.run(Test.java:35)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
Fri Mar 22 10:03:25 CST 2019
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
at java.lang.Thread.run(Thread.java:662)
null
java.lang.NumberFormatException: multiple points
Fri Mar 22 10:03:25 CST 2019 at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)

at java.lang.Double.parseDouble(Double.java:510)
at java.text.DigitList.getDouble(DigitList.java:151)Fri Mar 22 10:03:25 CST 2019

at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
Fri Mar 22 10:03:25 CST 2019
at com.resoft.original.framework.common.OriginalDateUtils.parseDate(OriginalDateUtils.java:275)
at com.resoft.original.framework.common.Test$1.run(Test.java:35)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)

......

java.lang.NumberFormatException: For input string: "1.01"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Long.parseLong(Long.java:419)
at java.lang.Long.parseLong(Long.java:468)
at java.text.DigitList.getLong(DigitList.java:177)
Fri Mar 22 10:03:25 CST 2019
at java.text.DecimalFormat.parse(DecimalFormat.java:1297)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
at com.resoft.original.framework.common.OriginalDateUtils.parseDate(OriginalDateUtils.java:275)
Fri Mar 22 10:03:25 CST 2019
at com.resoft.original.framework.common.Test$1.run(Test.java:35)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
at java.lang.Thread.run(Thread.java:662)
null
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Long.parseLong(Long.java:431)
Fri Mar 22 10:03:25 CST 2019
at java.lang.Long.parseLong(Long.java:468)
at java.text.DigitList.getLong(DigitList.java:177)
at java.text.DecimalFormat.parse(DecimalFormat.java:1297)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
Fri Mar 22 10:03:25 CST 2019
at com.resoft.original.framework.common.OriginalDateUtils.parseDate(OriginalDateUtils.java:275)
at com.resoft.original.framework.common.Test$1.run(Test.java:35)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
at java.lang.Thread.run(Thread.java:662)
null
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Long.parseLong(Long.java:431)
at java.lang.Long.parseLong(Long.java:468)
at java.text.DigitList.getLong(DigitList.java:177)
at java.text.DecimalFormat.parse(DecimalFormat.java:1297)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
at com.resoft.original.framework.common.OriginalDateUtils.parseDate(OriginalDateUtils.java:275)
at com.resoft.original.framework.common.Test$1.run(Test.java:35)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
at java.lang.Thread.run(Thread.java:662)
null
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019

分析错误日志有如下错误

  • 会出现日期不对的情况
  • java.lang.NumberFormatException: For input string xxx
  • java.lang.NumberFormatException: multiple points

错误原因分析

首先看代码是如何写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static Map<DateFormat, SimpleDateFormat> dateFormaters = new HashMap<DateFormat, SimpleDateFormat>(3, 0.2f);

private static SimpleDateFormat getDateFormater(DateFormat format) {
SimpleDateFormat dateFormater = null;

if ((dateFormater = dateFormaters.get(format)) == null) {
dateFormater = new SimpleDateFormat(format.format);
dateFormaters.put(format, dateFormater);
}

return dateFormater;
}

public static java.util.Date parseDate(String dateStr) {
return getDateFormater(DateFormat.YMDHMS_HYPHEN).parse(
dateStr);
}

DateFormat是枚举,定义了各种日期格式
代码本质还是获取一个 静态SimpleDateFormat 对象,引入Map是为了提高效率。
那么在多线程的情况下SimpleDateFormat对象实例就会 被多个线程共享 ,通过看源码找原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public Date parse(String text, ParsePosition pos) {

checkNegativeNumberExpression();

int start = pos.index;
int oldStart = start;
int textLength = text.length();

calendar.clear(); // Clears all the time fields

boolean[] ambiguousYear = { false };

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:
if (start >= textLength || text.charAt(start) != (char) count) {
pos.index = oldStart;
pos.errorIndex = start;
return null;
}
start++;
break;

case TAG_QUOTE_CHARS:
while (count-- > 0) {
if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
pos.index = oldStart;
pos.errorIndex = start;
return null;
}
start++;
}
break;

default:
boolean obeyCount = false;
boolean useFollowingMinusSignAsDelimiter = false;

if (i < compiledPattern.length) {
int nextTag = compiledPattern[i] >>> 8;
if (!(nextTag == TAG_QUOTE_ASCII_CHAR || nextTag == TAG_QUOTE_CHARS)) {
obeyCount = true;
}

if (hasFollowingMinusSign && (nextTag == TAG_QUOTE_ASCII_CHAR || nextTag == TAG_QUOTE_CHARS)) {
int c;
if (nextTag == TAG_QUOTE_ASCII_CHAR) {
c = compiledPattern[i] & 0xff;
} else {
c = compiledPattern[i + 1];
}

if (c == minusSign) {
useFollowingMinusSignAsDelimiter = true;
}
}
}
start = subParse(text, start, tag, count, obeyCount, ambiguousYear, pos, useFollowingMinusSignAsDelimiter);
if (start < 0) {
pos.index = oldStart;
return null;
}
}
}

pos.index = start;
Date parsedDate;
try {
if (ambiguousYear[0]) {
Calendar savedCalendar = (Calendar) calendar.clone();
parsedDate = calendar.getTime();
if (parsedDate.before(defaultCenturyStart)) {
savedCalendar.set(Calendar.YEAR, defaultCenturyStartYear + 100);
parsedDate = savedCalendar.getTime();
}
} else
parsedDate = calendar.getTime();
} catch (IllegalArgumentException e) {
pos.errorIndex = start;
pos.index = oldStart;
return null;
}

return parsedDate;
}

重点关注 calendar.clear() , calendar.getTime() 方法,SimpleDateFormat的parse方法实际操作的就是 Calendar

因为我们声明SimpleDateFormat为static变量,那么它的Calendar变量也就是一个共享变量,可以被多个线程访问。

假设线程A执行完calendar.clear()方法,这时候被挂起,线程B获得CPU执行权。线程B执行到了calendar.getTime()方法就获取到空值了,而这就是引发问题的根源,出现时间不对,线程挂死等等。

其实SimpleDateFormat源码上作者也给过我们提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 * <h4><a name="synchronization">Synchronization</a></h4>
*
* <p>
* Date formats are not synchronized.
* 日期格式不同步。
* It is recommended to create separate format instances for each thread.
* 建议为每个线程创建单独的格式实例。
* If multiple threads access a format concurrently, it must be synchronized
* externally.
* 如果多个线程同时访问一种格式,则必须在外部同步该格式。
*
* @see <a href="http://java.sun.com/docs/books/tutorial/i18n/format/simpleDateFormat.html">Java Tutorial</a>
* @see java.util.Calendar
* @see java.util.TimeZone
* @see DateFormat
* @see DateFormatSymbols
* @version %I%, %G%
* @author Mark Davis, Chen-Lieh Huang, Alan Liu
*/
public class SimpleDateFormat extends DateFormat {

解决办法

  1. 只要在用的时候创建新实例,不用static修饰。

    1
    2
    3
    4
    public static Date parse(String strDate) throws ParseException{
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    return sdf.parse(strDate);
    }

    如上代码,仅在需要用到的地方创建一个新的实例,就没有线程安全问题,不过也加重了创建对象的负担,会频繁地创建和销毁对象,效率较低。

  2. 使用synchronized来SimpleDateFormat对象

    1
    2
    3
    4
    5
    6
    7
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String strDate) throws ParseException{
    synchronized(sdf){
    return sdf.parse(strDate);
    }
    }

    当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。

  3. 使用ThreadLocal

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
    };

    public static Date parse(String dateStr) throws ParseException {
    return threadLocal.get().parse(dateStr);
    }

    使用ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。

优化框架代码

作为框架代码必须要保证性能要求,用ThreadLocal来优化代码

1
2
3
4
5
6
7
8
9
10
11
12
private static ThreadLocal<java.text.DateFormat> threadLocal = new ThreadLocal<java.text.DateFormat>();

private static java.text.DateFormat getDateFormater(DateFormat format) {

java.text.DateFormat dateFormater = threadLocal.get();

if (dateFormater == null) {
dateFormater = new SimpleDateFormat(format.format);
threadLocal.set(dateFormater);
}
return dateFormater;
}

用同样的测试代码测试,结果如下

1
2
3
4
Fri Mar 22 10:03:25 CST 2019
Fri Mar 22 10:03:25 CST 2019
......
Fri Mar 22 10:03:25 CST 2019

结语

我们要站在巨人的肩膀上来做事,如果是jdk1.8 可以使用DateTimeFormatter对象来解析或格式化日期;或直接使用Joda-Time类库来处理时间相关问题。

参考

还在使用SimpleDateFormat?你的项目崩没?
深入理解Java:SimpleDateFormat安全的时间格式化