【优雅的避坑】不安全!别再共享SimpleDateFormat了
作者:行百里er
博客:https://chendapeng.cn (opens new window)
提示
这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!
# 0x01 开场白
JDK文档中已经明确表明了SimpleDateFormat不应该用在多线程场景中:
Synchronization
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.
然而,并不是所有Javaer都关注到了这句话,依然使用如下的方式进行日期时间格式化:
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
@Test
public void longLongAgo() {
String dateStr = sdf.format(new Date());
System.out.println("当前时间:" + dateStr);
}
2
3
4
5
6
7
一个线程这样做当然是没问题的。
既然官方文档都说了在多线程访问的场景中必须使用synchronized同步,那么就来验证一下,多线程场景下使用SimpleDateFormat会出现什么问题。
# 0x02 重现多线程场景使用SimpleDateFormat问题
定义一个线程池,跑多个线程执行对当前日期格式化的操作
/**
* 定义static的SimpleDateFormat,所有线程共享
**/
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 定义线程池
**/
private static ExecutorService threadPool = new ThreadPoolExecutor(16,
100,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1024),
new ThreadFactoryBuilder().setNameFormat("[线程-%d]").build(),
new ThreadPoolExecutor.AbortPolicy()
);
@SneakyThrows
@Test
public void testFormat() {
Set<String> results = Collections.synchronizedSet(new HashSet<>());
// 每个线程都执行“给日期加上一个天数”的操作,每个线程加的天数均不一样,
// 这样当THREAD_NUMBERS个线程执行完毕后,应该有THREAD_NUMBERS个结果才是正确的
for (int i = 0; i < THREAD_NUMBERS; i++) {
Calendar calendar = Calendar.getInstance();
int addDay = i;
threadPool.execute(() -> {
calendar.add(Calendar.DATE, addDay);
String result = sdf.format(calendar.getTime());
results.add(result);
});
}
//保证线程执行完
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
//最后打印结果
System.out.println("希望:" + THREAD_NUMBERS + ",实际:" + results.size());
}
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
正常情况下,以上代码results.size()
的结果应该是THREAD_NUMBERS。但是实际执行结果是一个小于该值的数字。
上面是format()
方法出现的问题,同样,SimpleDateFormat
的parse()
方法也会出现线程不安全的问题:
@SneakyThrows
@Test
public void testParse() {
String dateStr = "2020-10-22 08:08:08";
for (int i = 0; i < 20; i++) {
threadPool.execute(() -> {
try {
Date date = sdf.parse(dateStr);
System.out.println(Thread.currentThread().getName() + "---" + date);
} catch (ParseException e) {
e.printStackTrace();
}
});
}
//保证线程执行完
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
运行结果:
[线程-0]---Thu May 22 08:00:08 CST 2228
[线程-3]---Sun Oct 22 08:08:08 CST 8000
[线程-4]---Thu Oct 22 08:08:08 CST 2020
[线程-5]---Thu Oct 22 08:08:08 CST 2020
Exception in thread "[线程-1]" Exception in thread "[线程-2]" java.lang.NumberFormatException: For input string: "101.E1012E2"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
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:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.xblzer.tryout.SimpleDateFormatTest.lambda$testParse$1(SimpleDateFormatTest.java:78)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: "101.E1012E2"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
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:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.xblzer.tryout.SimpleDateFormatTest.lambda$testParse$1(SimpleDateFormatTest.java:78)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
[线程-8]---Wed Jan 22 08:09:28 CST 2020
[线程-11]---Sat Jan 25 16:08:08 CST 2020
[线程-9]---Thu Oct 22 08:08:08 CST 2020
Exception in thread "[线程-12]" java.lang.NumberFormatException: For input string: ""
[线程-10]---Thu Oct 22 08:08:08 CST 2020
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:1869)
[线程-13]---Thu Oct 22 08:08:08 CST 2020
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.xblzer.tryout.SimpleDateFormatTest.lambda$testParse$1(SimpleDateFormatTest.java:78)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
[线程-14]---Thu Oct 22 08:08:08 CST 2020
at java.lang.Thread.run(Thread.java:748)
[线程-16]---Thu Oct 22 08:08:08 CST 2020
[线程-18]---Thu Oct 22 08:08:08 CST 2020
[线程-16]---Thu Oct 22 08:08:08 CST 2020
[线程-18]---Thu Oct 22 08:08:08 CST 2020
Exception in thread "[线程-0]" java.lang.NumberFormatException: For input string: ""
[线程-16]---Thu Oct 22 08:08:08 CST 2020
[线程-17]---Thu Oct 22 08:08:08 CST 2020
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 com.xblzer.tryout.SimpleDateFormatTest.lambda$testParse$1(SimpleDateFormatTest.java:78)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
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
不仅有的线程解析的结果不正确,甚至有一些线程还出现了异常!
# 0x03 原因分析
原因就是因为 SimpleDateFormat
作为一个非线程安全的类,被当做了static
共享变量在多个线程中进行使用,这就出现了线程安全问题。
来跟一下源码。
format(Date date)
方法来源于类DateFormat
中的如下方法:
public final String format(Date date)
{
return format(date, new StringBuffer(),
DontCareFieldPosition.INSTANCE).toString();
}
2
3
4
5
调用abstract StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition)
public abstract StringBuffer format(Date date, StringBuffer toAppendTo,
FieldPosition fieldPosition);
2
这是一个抽象方法,具体的实现看SimpleDateFormat
类中的实现:
// 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;
}
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
大家看到了吧,format
方法在执行过程中,会使用一个成员变量
calendar
来保存时间。这就是问题的关键所在。
由于我们在声明SimpleDateFormat sdf
的时候,使用的是static
定义的,所以这个sdf
就是一个共享的变量,那么SimpleDateFormat
中的calendar
也可以被多个线程访问到。
例如,[线程-1]刚刚执行完calendar.setTime
把时间设置成 2020-10-22,还没执行完呢,[线程-2]又执行了calendar.setTime
把时间改成了 2020-10-23。此时,[线程-1]继续往下执行,执行calendar.getTime
得到的时间就是[线程-2]改过之后的。也就是说[线程-1]的setTime的结果被无情的无视了...
# 0x04 日期格式化的正确姿势
# 姿势1 使用synchronized
用synchronized
对共享变量加同步锁,使多个线程排队按照顺序执行,从而避免多线程并发带来的线程安全问题。
@SneakyThrows
@Test
public void testWithSynchronized() {
Set<String> results = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < THREAD_NUMBERS; i++) {
Calendar calendar = Calendar.getInstance();
int addDays = i;
threadPool.execute(() -> {
synchronized (sdf) {
calendar.add(Calendar.DATE, addDays);
String result = sdf.format(calendar.getTime());
//System.out.println(Thread.currentThread().getName() + "---" + result);
results.add(result);
}
});
}
//保证线程执行完
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
//最后打印结果
System.out.println("希望:" + THREAD_NUMBERS + ",实际:" + results.size());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 姿势2 将SimpleDateFormat设置成局部变量使用
局部变量不会被多个线程共享,也可以避免线程安全问题。
@SneakyThrows
@Test
public void testWithLocalVar() {
Set<String> results = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < THREAD_NUMBERS; i++) {
Calendar calendar = Calendar.getInstance();
int addDays = i;
threadPool.execute(() -> {
SimpleDateFormat localSdf = new SimpleDateFormat("yyyy-MM-dd");
calendar.add(Calendar.DATE, addDays);
String result = localSdf.format(calendar.getTime());
//System.out.println(Thread.currentThread().getName() + "---" + result);
results.add(result);
});
}
//保证线程执行完
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
//最后打印结果
System.out.println("希望:" + THREAD_NUMBERS + ",实际:" + results.size());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
每个线程都定义自己的变量SimpleDateFormat localSdf
,格式化localSdf.format(calendar.getTime())
,不会有线程安全问题。
# 姿势3 使用ThreadLocal
ThreadLocal
的目的是确保每个线程都可以得到一个自己的 SimpleDateFormat
的对象,所以也不会出现多线程之间的竞争问题。
/**
* 定义线程数量
**/
private static final int THREAD_NUMBERS = 50;
/**
* 定义ThreadLocal<SimpleDateFormat>,每个线程都有一个独享的对象
**/
private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<>();
/**
* 定义线程池
**/
private static ExecutorService threadPool = new ThreadPoolExecutor(16,
100,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1024),
new ThreadFactoryBuilder().setNameFormat("[线程-%d]").build(),
new ThreadPoolExecutor.AbortPolicy()
);
/**
* 延迟加载SimpleDateFormat
**/
private static SimpleDateFormat getDateFormat() {
SimpleDateFormat dateFormat = dateFormatThreadLocal.get();
if (dateFormat == null) {
dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormatThreadLocal.set(dateFormat);
}
return dateFormat;
}
@SneakyThrows
@Test
public void testFormatWithThreadLocal() {
Set<String> results = Collections.synchronizedSet(new HashSet<>());
// 每个线程都执行“给日期加上一个天数”的操作,每个线程加的天数均不一样,
// 这样当THREAD_NUMBERS个线程执行完毕后,应该有THREAD_NUMBERS个结果才是正确的
for (int i = 0; i < THREAD_NUMBERS; i++) {
Calendar calendar = Calendar.getInstance();
int addDay = i;
threadPool.execute(() -> {
calendar.add(Calendar.DATE, addDay);
//获取ThreadLocal中的本地SimpleDateFormat副本
String result = getDateFormat().format(calendar.getTime());
results.add(result);
});
}
//保证线程执行完
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
//最后打印结果
System.out.println("希望:" + THREAD_NUMBERS + ",实际:" + results.size());
}
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
关键点就是
getDateFormat().format(calendar.getTime());
getDateFormat()拿到属于自己线程的SimpleDateFormat对象。
运行结果:
# 姿势4 使用DateTimeFormatter
Java 8之后,JDK提供了DateTimeFormatter
类:
它也可以进行事件、日期的格式化,并且它是不可变的、线程安全的。
结合Java 8的LocalDateTime
时间操作工具类进行测试验证:
Java 8的LocalDate、LocalTime、LocalDateTime进一步加强了对日期和时间的处理。
/**
* 定义线程数量
**/
private static final int THREAD_NUMBERS = 50;
private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* 定义线程池
**/
private static ExecutorService threadPool = new ThreadPoolExecutor(16,
100,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1024),
new ThreadFactoryBuilder().setNameFormat("[线程-%d]").build(),
new ThreadPoolExecutor.AbortPolicy()
);
@SneakyThrows
@Test
public void testDateTimeFormatter() {
Set<String> results = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < THREAD_NUMBERS; i++) {
//这样写为了能用Lambda表达式
LocalDateTime[] now = {LocalDateTime.now()};
int addDay = i;
threadPool.execute(() -> {
now[0] = now[0].plusDays(addDay);
//System.out.println(Thread.currentThread().getName() + "====" + now[0]);
String result = now[0].format(formatter);
results.add(result);
});
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
System.out.println("希望:" + THREAD_NUMBERS + ",实际:" + results.size());
}
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
结果验证:
# 0x05 小结
SimpleDateFormat存在线程安全问题,使用以下几种方式解决该问题。
- 加synchronized同步锁。并发量大的时候会有性能问题,线程阻塞。
- 将SimpleDateFormat设置为局部变量。会频繁的创建和销毁对象,性能较低。
- 使用ThreadLocal。推荐使用。
- 使用Java 8新特性DateTimeFormatter。推荐使用。
# 优雅的避坑-未完待续
# 最后
欢迎并感谢关注我的微信公众号 行百里er,
回复 java 关键字,您将获得避坑系列原创文章:
还有Java精品pdf: