【优雅的避坑】不安全!别再共享SimpleDateFormat变量了
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()
方法出现的问题,同样,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 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变量了相关推荐
- Opengrok实践,踩坑才能避坑
如果你的项目需要检索源码,那么用Opengrok时不错的选择! Opengrok是开源的,java开发,如果是java开发的小伙伴,可以很方便的进行二次开发~~~ 下面先来说说Opengrok的部署: ...
- 到 Google 面试去!开发者必读的避坑指南
Google 一直是许多开发者心驰神往的地方,本文作者分享了自己面试 Google 的经历,尽管面试挂掉了,但有一些避坑的技巧仍然值得我们学习. 作者 | sochix 译者 | 明明如月,责编 | ...
- 优维科技招商基金 | 招商基金DevSecOps实践与避坑指南
8月19日-20日,为期两天的2022GOPS全球运维大会完美落幕.优维科技作为大会的金牌合作伙伴,参与了此次大会,并在现场展会带来优维科技EasyOps®一体化运维平台的全新解决方案与最佳实践参考. ...
- 新人赛《金融风控贷款违约》避坑指南!
↑↑↑关注后"星标"Datawhale 每日干货 & 每月组队学习,不错过 Datawhale干货 作者:陶旭东,北京师范大学,Datawhale成员 一.背景介绍 本文以 ...
- Linux重定向和管道符使用避坑指南
本文就分享一下我在实践中使用重定向和管道符遇到的一些坑,搞明白一些底层原理,写脚本的效率能提升不少. 我很喜欢 Linux 系统,尤其是 Linux 的一些设计很漂亮,比如可以将一些复杂的问题分解成若 ...
- 17条避坑指南:一份来自谷歌的数据库经验贴
点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 来源 | https://medium.com/@rak ...
- 怎么把原来的墙拆掉_电视墙避坑指南要收好!拆掉重装太心累...
电视墙是家里装修最为重要的一个地方. 相信很多人都想要把电视墙装修得简单又大气,而且还是容易搞卫生的整洁类型~但是,电视墙贴砖过程中,有很多坑需要注意,一不小心就要像下面的业主一样,拆掉重装. 业主反 ...
- VMProtect SDK完全避坑指南
文章目录 前言 编译VMProtect Demo 生成机器码 替换密钥对 生成序列号 总结 前言 在编写软件的时候,通常会有这样一个需求,需要对自己写的软件实现一机一码加密保护,并且最好能够限制使用时 ...
- python搭建项目结构_Django搭建项目实战与避坑细节详解
Django 开发项目是很快的,有多快?看完本篇文章,你就知道了. 安装 Django 前提条件:已安装 Python. Django 使用 pip 命令直接就可以安装: pip install dj ...
最新文章
- 【OkHttp】OkHttp 简介 ( OkHttp 框架特性 | Http 版本简介 )
- boost::multiprecision模块cpp_dec_float_100相关的测试程序
- Python机器学习:评价分类结果004F1score
- 数据库工作笔记016---Redis、Memcache和MongoDB的区别
- Windows 10 环境VS报表rdlc 中文乱码解决方案
- 如何在 Internet Explorer 中禁用和使用 ADODB.Stream 对象
- matlab定义变量var,设置变量数据类型 - MATLAB setvartype
- MathWorks 中国
- Web开发必须知道的知识点
- 交换机设备登录账号权限1_Stelnet(ssh)登陆华为交换机配置教程
- nfine mysql_全开源版NFine快速开发框架C#源码
- 硬盘突然变raw格式_硬盘突然变成RAW格式解决办法
- SU2 CFD代码阅读
- 开源高手推荐 十大最流行开源软件
- Python例题:设计一个工资(月薪)结算系统
- 加薪不如发奖金? 穆穆-movno1
- MacOs 更改锁屏快捷键
- windows10 javac错误:javac不是内部或外部命令 也不是可运行的程序
- SparkStreaming通过读取文件动态黑名单过滤
- MySQL 数据类型BINARY和VARBINARY
- 一个逗比 程序员 web前端的理想!