SimpleDateFormat的线程安全问题与解决方案 Dear 丶 2022-04-05 10:40 239阅读 0赞 **[SimpleDateFormat的线程安全问题与解决方案][SimpleDateFormat]** 1. 原因 SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的.这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用, 并且, 观察 sdf.parse() 方法,你会发现有如下的调用: ![复制代码][copycode.gif] Date parse() { calendar.clear(); // 清理calendar ... // 执行一些操作, 设置 calendar 的日期什么的 calendar.getTime(); // 获取calendar的时间 } ![复制代码][copycode.gif] 这里会导致的问题就是, 如果 线程A 调用了 sdf.parse(), 并且进行了 calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(), 这时候线程B也执行了sdf.clear()方法, 这样就导致线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear() 后被挂起, 这时候B 开始调用sdf.parse()并顺利i结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date 2. 问题重现 ![复制代码][copycode.gif] import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * @author zhenwei.liu created on 2013 13-8-29 下午5:35 * @version $Id$ */ public class DateFormatTest extends Thread { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); private String name; private String dateStr; private boolean sleep; public DateFormatTest(String name, String dateStr, boolean sleep) { this.name = name; this.dateStr = dateStr; this.sleep = sleep; } @Override public void run() { Date date = null; if (sleep) { try { TimeUnit.MILLISECONDS.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } try { date = sdf.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } System.out.println(name + " : date: " + date); } public static void main(String[] args) throws InterruptedException { ExecutorService executor = Executors.newCachedThreadPool(); // A 会sleep 2s 后开始执行sdf.parse() executor.execute(new DateFormatTest("A", "1991-09-13", true)); // B 打了断点,会卡在方法中间 executor.execute(new DateFormatTest("B", "2013-09-13", false)); executor.shutdown(); } } ![复制代码][copycode.gif] 使用Debug模式执行这段代码,并在sdf.parse()方法里打上断点 ![复制代码][copycode.gif] parse() { calendar.clear() // 这里打一个断点 calendar.getTime() } ![复制代码][copycode.gif] 过程: 1) 首先A线程跑起来以后会进入sleep 2) B线程跑起来, 卡在断点处 3) A线程醒过来, 执行 calendar.clear(), 并将设置sdf.calendar的date为1991-09-13, 此时 A B 的 calendar 都为 1991-09-13 4) 让断点继续执行, 输出如下 > A : date: Fri Sep 13 00:00:00 CDT 1991 > B : date: Fri Sep 13 00:00:00 CDT 1991 这并不是我们期待的结果 3. 解决方案 最简单的解决方案我们可以把static去掉,这样每个新的线程都会有一个自己的sdf实例,从而避免线程安全的问题 然而,使用这种方法,在高并发的情况下会大量的new sdf以及销毁sdf,这样是非常耗费资源的 在并发情况下,网站的请求任务与线程执行情况大概可以理解为如下 ![31202030-9575425dbccd4548a104a0f47cb4b6a9.png][] 例如Tomcat的线程池的最大Thread数为4, 现在需要执行的任务有1000个(理解为有1000个用户点了你的网站的某个功能), 而这1000个任务都会用到我们写的日期函数处理类 A) 假如说日期函数处理类使用的是new SimpleDateFormat的方法,那么这里就会有1000次sdf的创建和销毁 B) Java中提供了一种ThreadLocal的解决方案,它的工作方式是,每个线程只会有一个实例,也就是说我们执行完这1000个任务,总共只会实例化4个sdf. 而且,它并不会有多线程的并发问题,因为,单个线程执行任务肯定是顺序的,例如Thread \#1负责执行Task \#1-\#250, 那么他是顺序而执行Task \#1-\#250 而Thread \#2拥有自己的sdf实例,他也是顺序执行任务 Task \#251-\#500, 以此类推 下面是一个使用ThreadLocal解决sdf多线程问题的例子 ![复制代码][copycode.gif] import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @author zhenwei.liu created on 2013 13-8-29 下午5:35 * @version $Id$ */ public class DateUtil { /** 锁对象 */ private static final Object lockObj = new Object(); /** 存放不同的日期模板格式的sdf的Map */ private static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<String, ThreadLocal<SimpleDateFormat>>(); /** * 返回一个ThreadLocal的sdf,每个线程只会new一次sdf * * @param pattern * @return */ private static SimpleDateFormat getSdf(final String pattern) { ThreadLocal<SimpleDateFormat> tl = sdfMap.get(pattern); // 此处的双重判断和同步是为了防止sdfMap这个单例被多次put重复的sdf if (tl == null) { synchronized (lockObj) { tl = sdfMap.get(pattern); if (tl == null) { // 只有Map中还没有这个pattern的sdf才会生成新的sdf并放入map System.out.println("put new sdf of pattern " + pattern + " to map"); // 这里是关键,使用ThreadLocal<SimpleDateFormat>替代原来直接new SimpleDateFormat tl = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { System.out.println("thread: " + Thread.currentThread() + " init pattern: " + pattern); return new SimpleDateFormat(pattern); } }; sdfMap.put(pattern, tl); } } } return tl.get(); } /** * 是用ThreadLocal<SimpleDateFormat>来获取SimpleDateFormat,这样每个线程只会有一个SimpleDateFormat * * @param date * @param pattern * @return */ public static String format(Date date, String pattern) { return getSdf(pattern).format(date); } public static Date parse(String dateStr, String pattern) throws ParseException { return getSdf(pattern).parse(dateStr); } } ![复制代码][copycode.gif] 测试类 ![复制代码][copycode.gif] import java.text.ParseException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * @author zhenyu.nie created on 2013 13-8-26 下午2:13 * @version 1.0.0 */ public class Test { public static void main(String[] args) { final String patten1 = "yyyy-MM-dd"; final String patten2 = "yyyy-MM"; Thread t1 = new Thread() { @Override public void run() { try { DateUtil.parse("1992-09-13", patten1); } catch (ParseException e) { e.printStackTrace(); } } }; Thread t2 = new Thread() { @Override public void run() { try { DateUtil.parse("2000-09", patten2); } catch (ParseException e) { e.printStackTrace(); } } }; Thread t3 = new Thread() { @Override public void run() { try { DateUtil.parse("1992-09-13", patten1); } catch (ParseException e) { e.printStackTrace(); } } }; Thread t4 = new Thread() { @Override public void run() { try { DateUtil.parse("2000-09", patten2); } catch (ParseException e) { e.printStackTrace(); } } }; Thread t5 = new Thread() { @Override public void run() { try { DateUtil.parse("2000-09-13", patten1); } catch (ParseException e) { e.printStackTrace(); } } }; Thread t6 = new Thread() { @Override public void run() { try { DateUtil.parse("2000-09", patten2); } catch (ParseException e) { e.printStackTrace(); } } }; System.out.println("单线程执行: "); ExecutorService exec = Executors.newFixedThreadPool(1); exec.execute(t1); exec.execute(t2); exec.execute(t3); exec.execute(t4); exec.execute(t5); exec.execute(t6); exec.shutdown(); sleep(1000); System.out.println("双线程执行: "); ExecutorService exec2 = Executors.newFixedThreadPool(2); exec2.execute(t1); exec2.execute(t2); exec2.execute(t3); exec2.execute(t4); exec2.execute(t5); exec2.execute(t6); exec2.shutdown(); } private static void sleep(long millSec) { try { TimeUnit.MILLISECONDS.sleep(millSec); } catch (InterruptedException e) { e.printStackTrace(); } } } ![复制代码][copycode.gif] 输出 > 单线程执行: > put new sdf of pattern yyyy-MM-dd to map > thread: Thread\[pool-1-thread-1,5,main\] init pattern: yyyy-MM-dd > put new sdf of pattern yyyy-MM to map > thread: Thread\[pool-1-thread-1,5,main\] init pattern: yyyy-MM > 双线程执行: > thread: Thread\[pool-2-thread-1,5,main\] init pattern: yyyy-MM-dd > thread: Thread\[pool-2-thread-2,5,main\] init pattern: yyyy-MM > thread: Thread\[pool-2-thread-1,5,main\] init pattern: yyyy-MM > thread: Thread\[pool-2-thread-2,5,main\] init pattern: yyyy-MM-dd 从输出我们可以看出: 1) 1个线程执行这6个任务的时候,这个线程首次使用过的时候会new一个新的sdf,并且以后都一直用这个sdf,而不是每次处理任务都新建一个新的sdf 2) 2个线程执行6个任务的时候也是同理,但是2个线程的sdf是分开的,每个线程都有自己的"yyyy-MM-dd", "yyyy-MM"的sdf,所以他们不会有线程安全安全问题 试想,如果使用的是new的实现方法,那么不管是用1个线程去执行,还是用2个线程去执行这6个任务,都需要new 6个sdf 分类: [JAVA][] 好文要顶 关注我 收藏该文 ![icon_weibo_24.png][] ![wechat.png][] [![20160318195649.png][]][20160318195649.png 1] [ZimZz][20160318195649.png 1] [关注 - 1][- 1] [粉丝 - 174][- 174] \+加关注 7 2 [« ][Link 1] 上一篇:[未解决问题备份][Link 1] [» ][Link 2] 下一篇:[乐观锁与悲观锁][Link 2] posted on 2013-08-29 23:52 [ZimZz][] 阅读(52110) 评论(15) [编辑][Link 3] [收藏][Link 4] Feedback [\#1楼][1] 2014-11-26 14:09 | [doctor\_q][doctor_q] [ ][Link 5] 代码太长了。。。 支持(0)反对(0) [\#2楼][2] 2016-08-11 20:22 | [java外行][java] [ ][Link 6] 很不错,就是不知道怎么转载 支持(0)反对(0) [\#3楼][3] 2016-08-15 18:26 | [dragonkiiiiiiiing][] [ ][Link 7] 你的threadLocal用法不对,threadLocal不是这样用的 支持(3)反对(1) [\#4楼][4] 2016-10-19 11:40 | [Mike.W][] [ ][Link 8] [@][3] dragonkiiiiiiiing [引用][3]你的threadLocal用法不对,threadLocal不是这样用的 是这样用的,没有问题,请看官方API文档。 支持(0)反对(1) [\#5楼][5] 2016-12-09 16:51 | [ysjjovo][] [ ][Link 9] 我写了一个无同步的实现,博主帮看下这样是否可行 package beans; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; public class DateUtil \{ private static final HashMap<String,ThreadLocal<SimpleDateFormat>> map=new HashMap<String, ThreadLocal<SimpleDateFormat>>(); private static String SHORT="yyyy-MM-dd"; private static String LONG="yyyy-MM-dd HH:mm:ss"; private static String EQUAL="yyyy-MM-dd HH:mm:ss.SSS"; static \{ map.put(SHORT,new ThreadLocal<SimpleDateFormat>()\{ @Override public SimpleDateFormat initialValue()\{ return new SimpleDateFormat(SHORT); \} \}); map.put(LONG,new ThreadLocal<SimpleDateFormat>()\{ @Override public SimpleDateFormat initialValue()\{ return new SimpleDateFormat(LONG); \} \}); map.put(EQUAL,new ThreadLocal<SimpleDateFormat>()\{ @Override public SimpleDateFormat initialValue()\{ return new SimpleDateFormat(EQUAL); \} \}); \} public static String toShort(Date date)\{ return map.get(SHORT).get().format(date); \} public static Date toShortDate(String dateString) throws ParseException \{ return map.get(SHORT).get().parse(dateString); \} public static String toLong(Date date)\{ return map.get(LONG).get().format(date); \} public static Date toLongDate(String dateString) throws ParseException \{ return map.get(LONG).get().parse(dateString); \} public static String toEqual(Date date) throws ParseException \{ return map.get(EQUAL).get().format(date); \} public static Date toEqualDate(String dateString) throws ParseException \{ return map.get(EQUAL).get().parse(dateString); \} \} 支持(0)反对(1) [\#6楼][6] 2017-02-17 16:32 | [扭断翅膀的猪][Link 10] [ ][Link 11] [@][5] ysjjovo 你觉得你和楼主写的有本质区别吗 支持(0)反对(0) [\#7楼][7] 2017-02-17 16:32 | [扭断翅膀的猪][Link 10] [ ][Link 11] 看一下能不能改变昵称 支持(0)反对(0) [\#8楼][8] 2017-05-04 14:55 | [junyi5257][] [ ][Link 12] 这个断点--有点不好找,具体为止 JDK1.7版本java.text.CalendarBuilder 的establish()方法中,有 clear(), 可以在establish()返回值为止设置断点。 支持(0)反对(0) [\#9楼][9] 2017-10-12 16:31 | [Frostmourn][] [ ][Link 13] [@][4] Mike.W 都用了同步锁还要ThreadLocal干嘛,既然用了ThreadLocal就不应该再用同步锁 Map<String, ThreadLocal<SimpleDateFormat>>你这个完全弄反了,应该是ThreadLocal<Map<String, SimpleDateFormat>>这样每个线程都是一个独立的Map 支持(0)反对(1) [\#10楼][10] 2017-11-20 14:07 | [龙须子][Link 14] [ ][Link 15] 同意\#3楼的观点,你的threadLocal用法不对,threadLocal不是这样用的,既然用了锁,还要这么麻烦再用threadlocal? 支持(0)反对(1) [\#11楼][11] 2017-11-22 08:41 | [college6666][] [ ][Link 16] [@][10] 龙须子 你没看清题目吗?threadlocal是保证每个线程只有一个format实例,同步跟着没关系,同步只是同步执行,没其他作用 支持(0)反对(0) [\#12楼][12] 2017-11-22 14:47 | [龙须子][Link 14] [ ][Link 15] [@][11] college6666 threadlocal是为了保证每个线程只有一个format实例? 那只是为了给共享format对象的每个线程提供一个它的副本,并不是为每个线程创建一个实例,你那个synchronized为了保证map线程安全,之前看错了 支持(0)反对(0) [\#13楼][13] 2018-07-03 14:17 | [Eyebrows\_cs][Eyebrows_cs] [ ][Link 17] <table> <tbody> <tr> <td> <p>1</p> <p>2</p> <p>3</p> <p>4</p> <p>5</p> <p>6</p> <p>7</p> <p>8</p> <p>9</p> <p>10</p> <p>11</p> <p>12</p> <p>13</p> <p>14</p> <p>15</p> <p>16</p> <p>17</p> </td> <td> <p><code>private</code> <code>static</code> <code>final</code> <code>Object lockObj = </code><code>new</code> <code>Object();</code></p> <p><code>private</code> <code>static</code> <code>ThreadLocal<HashMap<String, SimpleDateFormat>> threadLocal = </code><code>new</code> <code>ThreadLocal<>();</code></p> <p> </p> <p><code>public</code> <code>static</code> <code>SimpleDateFormat getSimpleDateFormat(</code><code>final</code> <code>String format) { </code></p> <p><code> </code><code>HashMap<String, SimpleDateFormat> sdfMap = threadLocal.get();</code></p> <p><code> </code><code>if</code> <code>(sdfMap == </code><code>null</code><code>) { </code></p> <p><code> </code><code>synchronized</code> <code>(lockObj) { </code></p> <p><code> </code><code>sdfMap = threadLocal.get();</code></p> <p><code> </code><code>if</code> <code>(sdfMap == </code><code>null</code><code>) { </code></p> <p><code> </code><code>System.out.println(</code><code>"create instance from #"</code> <code>+ Thread.currentThread().getId());</code></p> <p><code> </code><code>sdfMap = </code><code>new</code> <code>HashMap<>();</code></p> <p><code> </code><code>sdfMap.put(format, </code><code>new</code> <code>SimpleDateFormat(format));</code></p> <p><code> </code><code>}</code></p> <p><code> </code><code>}</code></p> <p><code> </code><code>}</code></p> <p><code> </code><code>return</code> <code>sdfMap.get(format);</code></p> <p><code>}</code></p> </td> </tr> </tbody> </table> 支持(0)反对(0) [\#14楼][14] 2018-08-02 17:09 | [cq\_fuqq][cq_fuqq] [ ][Link 18] 喜欢你对线程和任务原型图的理解。我再皮一下:业务场景是一大群人过河,船就是线程,少且有限,假设和中间龙王要收过河费,由于船包月了过河特权,所以不论上来多少人,统统免费通行 支持(0)反对(0) [\#15楼][15] 2018-09-26 16:57 | [华行天下][Link 19] [ ][Link 20] 博写的很容易误导人 1、private static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap 全局使用同一个map,得出的SimpleDateFormat是之前创建的。 2、既然使用ThreadLocal,使用同步锁是不是多此一举。 仅代表个人观点,欢迎指正。 [SimpleDateFormat]: https://www.cnblogs.com/zemliu/p/3290585.html [copycode.gif]: /images/20220405/1a8f684eae884ca3bd0087fa10a445c4.png [31202030-9575425dbccd4548a104a0f47cb4b6a9.png]: /images/20220405/b58b75841e0d4ff1aeb22f11081f4c67.png [JAVA]: https://www.cnblogs.com/zemliu/category/377383.html [icon_weibo_24.png]: http://common.cnblogs.com/images/icon_weibo_24.png [wechat.png]: /images/20220405/819d132126f64520b22aecb14bbd8131.png [20160318195649.png]: /images/20220405/e496ae420487410db81af8c2c27d231a.png [20160318195649.png 1]: http://home.cnblogs.com/u/zemliu/ [- 1]: http://home.cnblogs.com/u/zemliu/followees [- 174]: http://home.cnblogs.com/u/zemliu/followers [Link 1]: https://www.cnblogs.com/zemliu/p/3288252.html [Link 2]: https://www.cnblogs.com/zemliu/p/3295805.html [ZimZz]: https://www.cnblogs.com/zemliu/ [Link 3]: https://i.cnblogs.com/EditPosts.aspx?postid=3290585 [Link 4]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html# [1]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3072048 [doctor_q]: https://www.cnblogs.com/mixes/ [Link 5]: http://msg.cnblogs.com/send/doctor_q [2]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3487783 [java]: https://www.cnblogs.com/shaoyang/ [Link 6]: http://msg.cnblogs.com/send/java%E5%A4%96%E8%A1%8C [3]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3489811 [dragonkiiiiiiiing]: http://home.cnblogs.com/u/924495/ [Link 7]: http://msg.cnblogs.com/send/dragonkiiiiiiiing [4]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3535710 [Mike.W]: http://home.cnblogs.com/u/818547/ [Link 8]: http://msg.cnblogs.com/send/Mike.W [5]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3576820 [ysjjovo]: https://www.cnblogs.com/ysjjovo/ [Link 9]: http://msg.cnblogs.com/send/ysjjovo [6]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3621043 [Link 10]: http://home.cnblogs.com/u/1014942/ [Link 11]: http://msg.cnblogs.com/send/%E6%89%AD%E6%96%AD%E7%BF%85%E8%86%80%E7%9A%84%E7%8C%AA [7]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3621045 [8]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3683350 [junyi5257]: https://www.cnblogs.com/junyi0120/ [Link 12]: http://msg.cnblogs.com/send/junyi5257 [9]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3808531 [Frostmourn]: http://home.cnblogs.com/u/748516/ [Link 13]: http://msg.cnblogs.com/send/Frostmourn [10]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3845349 [Link 14]: https://www.cnblogs.com/ljhoracle/ [Link 15]: http://msg.cnblogs.com/send/%E9%BE%99%E9%A1%BB%E5%AD%90 [11]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3846980 [college6666]: http://home.cnblogs.com/u/1284625/ [Link 16]: http://msg.cnblogs.com/send/college6666 [12]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#3847399 [13]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#4011501 [Eyebrows_cs]: http://home.cnblogs.com/u/431627/ [Link 17]: http://msg.cnblogs.com/send/Eyebrows_cs [14]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#4034276 [cq_fuqq]: https://www.cnblogs.com/cqfuqq/ [Link 18]: http://msg.cnblogs.com/send/cq_fuqq [15]: http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html#4077079 [Link 19]: https://www.cnblogs.com/huaxingtianxia/ [Link 20]: http://msg.cnblogs.com/send/%E5%8D%8E%E8%A1%8C%E5%A4%A9%E4%B8%8B
还没有评论,来说两句吧...