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<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());
}

正常情况下,以上代码results.size()的结果应该是THREAD_NUMBERS。但是实际执行结果是一个小于该值的数字。

上面是format()方法出现的问题,同样,SimpleDateFormatparse()方法也会出现线程不安全的问题:

@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 2020at 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 2020at 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 2020at 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 2020at 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 listcalendar.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;
}

大家看到了吧,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 将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());
}

每个线程都定义自己的变量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());
}

关键点就是

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());
}

结果验证:

0x05 小结

SimpleDateFormat存在线程安全问题,使用以下几种方式解决该问题。

  • 加synchronized同步锁。并发量大的时候会有性能问题,线程阻塞。
  • 将SimpleDateFormat设置为局部变量。会频繁的创建和销毁对象,性能较低。
  • 使用ThreadLocal。推荐使用。
  • 使用Java 8新特性DateTimeFormatter。推荐使用。

优雅的避坑-未完待续

最后

欢迎关注我的微信公众号 行百里er,回复java,您将获得避坑系列原创文章:

还有Java精品pdf:

【优雅的避坑】不安全!别再共享SimpleDateFormat变量了相关推荐

  1. Opengrok实践,踩坑才能避坑

    如果你的项目需要检索源码,那么用Opengrok时不错的选择! Opengrok是开源的,java开发,如果是java开发的小伙伴,可以很方便的进行二次开发~~~ 下面先来说说Opengrok的部署: ...

  2. 到 Google 面试去!开发者必读的避坑指南

    Google 一直是许多开发者心驰神往的地方,本文作者分享了自己面试 Google 的经历,尽管面试挂掉了,但有一些避坑的技巧仍然值得我们学习. 作者 | sochix 译者 | 明明如月,责编 | ...

  3. 优维科技招商基金 | 招商基金DevSecOps实践与避坑指南

    8月19日-20日,为期两天的2022GOPS全球运维大会完美落幕.优维科技作为大会的金牌合作伙伴,参与了此次大会,并在现场展会带来优维科技EasyOps®一体化运维平台的全新解决方案与最佳实践参考. ...

  4. 新人赛《金融风控贷款违约》避坑指南!

    ↑↑↑关注后"星标"Datawhale 每日干货 & 每月组队学习,不错过 Datawhale干货 作者:陶旭东,北京师范大学,Datawhale成员 一.背景介绍 本文以 ...

  5. Linux重定向和管道符使用避坑指南

    本文就分享一下我在实践中使用重定向和管道符遇到的一些坑,搞明白一些底层原理,写脚本的效率能提升不少. 我很喜欢 Linux 系统,尤其是 Linux 的一些设计很漂亮,比如可以将一些复杂的问题分解成若 ...

  6. 17条避坑指南:一份来自谷歌的数据库经验贴

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 来源 | https://medium.com/@rak ...

  7. 怎么把原来的墙拆掉_电视墙避坑指南要收好!拆掉重装太心累...

    电视墙是家里装修最为重要的一个地方. 相信很多人都想要把电视墙装修得简单又大气,而且还是容易搞卫生的整洁类型~但是,电视墙贴砖过程中,有很多坑需要注意,一不小心就要像下面的业主一样,拆掉重装. 业主反 ...

  8. VMProtect SDK完全避坑指南

    文章目录 前言 编译VMProtect Demo 生成机器码 替换密钥对 生成序列号 总结 前言 在编写软件的时候,通常会有这样一个需求,需要对自己写的软件实现一机一码加密保护,并且最好能够限制使用时 ...

  9. python搭建项目结构_Django搭建项目实战与避坑细节详解

    Django 开发项目是很快的,有多快?看完本篇文章,你就知道了. 安装 Django 前提条件:已安装 Python. Django 使用 pip 命令直接就可以安装: pip install dj ...

最新文章

  1. 【OkHttp】OkHttp 简介 ( OkHttp 框架特性 | Http 版本简介 )
  2. boost::multiprecision模块cpp_dec_float_100相关的测试程序
  3. Python机器学习:评价分类结果004F1score
  4. 数据库工作笔记016---Redis、Memcache和MongoDB的区别
  5. Windows 10 环境VS报表rdlc 中文乱码解决方案
  6. 如何在 Internet Explorer 中禁用和使用 ADODB.Stream 对象
  7. matlab定义变量var,设置变量数据类型 - MATLAB setvartype - MathWorks 中国
  8. Web开发必须知道的知识点
  9. 交换机设备登录账号权限1_Stelnet(ssh)登陆华为交换机配置教程
  10. nfine mysql_全开源版NFine快速开发框架C#源码
  11. 硬盘突然变raw格式_硬盘突然变成RAW格式解决办法
  12. SU2 CFD代码阅读
  13. 开源高手推荐 十大最流行开源软件
  14. Python例题:设计一个工资(月薪)结算系统
  15. 加薪不如发奖金? 穆穆-movno1
  16. MacOs 更改锁屏快捷键
  17. windows10 javac错误:javac不是内部或外部命令 也不是可运行的程序
  18. SparkStreaming通过读取文件动态黑名单过滤
  19. MySQL 数据类型BINARY和VARBINARY
  20. 一个逗比 程序员 web前端的理想!

热门文章

  1. Java名片管理系统
  2. 实现网站对IP地址的限制访问
  3. 通过git提交网站到码云(gitee)并部署发布静态网站
  4. 如何将网站提交到百度的办法
  5. 使用ARP欺骗, 截取局域网中任意一台机器的网页请求,破解用户名密码等信息
  6. 在Word中隐藏文字
  7. KITTI结果评测流程
  8. 迅睿cms,迅睿cms程序系统,迅睿cms网站优化
  9. Android 控件开发之ToggleButton
  10. 对UART、RS232、485通信的理解