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);
}
一个线程这样做当然是没问题的。
既然官方文档都说了在多线程访问的场景中必须使用「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 results = Collections.synchronizedSet(new HashSet<>());// 每个线程都执行“给日期加上一个天数”的操作,每个线程加的天数均不一样,// 这样当THREAD_NUMBERS个线程执行完毕后,应该有THREAD_NUMBERS个结果才是正确的for (int i = 0; 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());
}
![d9028949f54047e9618df4cdb93fd26a.png](https://i-blog.csdnimg.cn/blog_migrate/2b4d23290eb3a97204e861e50f6d2e92.png)
正常情况下,以上代码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);
}
运行结果:
[线程-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)
不仅有的线程解析的结果不正确,甚至有一些线程还出现了异常!
0x03 原因分析
原因就是因为 SimpleDateFormat
作为一个非线程安全的类,被当做了static
「共享变量」在多个线程中进行使用,这就出现了「线程安全问题」。
来跟一下源码。
format(Date date)
方法来源于类DateFormat
中的如下方法:
public final String format(Date date){
return format(date, new StringBuffer(),
DontCareFieldPosition.INSTANCE).toString();
}
调用abstract StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition)
public abstract StringBuffer format(Date date, StringBuffer toAppendTo,
FieldPosition fieldPosition);
这是一个抽象方法,具体的实现看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 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;
}
大家看到了吧,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 results = Collections.synchronizedSet(new HashSet<>());for (int i = 0; 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());
}
![0b4f3b9b64d22baefb9c2f7c950673ac.png](https://i-blog.csdnimg.cn/blog_migrate/585a4aac7f84d78b0348e06ca2ad8b44.png)
姿势2 将SimpleDateFormat设置成局部变量使用
「局部变量不会被多个线程共享」,也可以避免线程安全问题。
@SneakyThrows
@Test
public void testWithLocalVar() {
Set results = Collections.synchronizedSet(new HashSet<>());for (int i = 0; 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());
}
每个线程都定义自己的变量SimpleDateFormat localSdf
,格式化localSdf.format(calendar.getTime())
,不会有线程安全问题。
![e596fb9bec257c016f2832cc634e3366.png](https://i-blog.csdnimg.cn/blog_migrate/16db4aed06edae377349bdcda4a745c2.png)
姿势3 使用ThreadLocal
ThreadLocal
的目的是确保每个线程都可以得到一个自己的 SimpleDateFormat
的对象,所以也不会出现多线程之间的竞争问题。
/**
* 定义线程数量
**/
private static final int THREAD_NUMBERS = 50;
/**
* 定义ThreadLocal,每个线程都有一个独享的对象
**/
private static ThreadLocal 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@Testpublic void testFormatWithThreadLocal() {
Set results = Collections.synchronizedSet(new HashSet<>());// 每个线程都执行“给日期加上一个天数”的操作,每个线程加的天数均不一样,// 这样当THREAD_NUMBERS个线程执行完毕后,应该有THREAD_NUMBERS个结果才是正确的for (int i = 0; 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());
}
关键点就是
getDateFormat().format(calendar.getTime());
getDateFormat()拿到属于自己线程的SimpleDateFormat对象。
运行结果:
![9b3765f266468b843d2c414cfc31f9df.png](https://i-blog.csdnimg.cn/blog_migrate/9efe31b14ce88e37f4cc0aeb12aaab29.png)
姿势4 使用DateTimeFormatter
Java 8之后,JDK提供了DateTimeFormatter
类:
![32288435c209a69f8d8eafa86cc29077.png](https://i-blog.csdnimg.cn/blog_migrate/4214e89bfef5402613b399e33afc6256.png)
它也可以进行事件、日期的格式化,并且它是「不可变的、线程安全的」。
结合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 results = Collections.synchronizedSet(new HashSet<>());for (int i = 0; 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());
}
结果验证:
![4737b0f59215eec4007e24b1f8a8d80c.png](https://i-blog.csdnimg.cn/blog_migrate/18896908cc4f3ab8c03c73751f4ae5bf.png)
小结
SimpleDateFormat存在线程安全问题,使用以下几种方式解决该问题。
- 加synchronized同步锁。并发量大的时候会有性能问题,线程阻塞。
- 将SimpleDateFormat设置为局部变量。会频繁的创建和销毁对象,性能较低。
- 使用ThreadLocal。推荐使用。
- 使用Java 8新特性DateTimeFormatter。推荐使用。
往期推荐
【优雅的避坑】避免HashMap扩容的正确姿势
【优雅的避坑】你的钱算错了!为什么0.1+0.2不等于0.3了!?
【优雅的避坑】不要轻易使用==比较两个Integer的值
优雅的避坑-从验证码功能代码优化到JVM栈和堆