部分程序员可能都遇到过SimpleDateFormat的线程安全问题,在JDK文档中也说明了该类是线程非安全的,建议对于每个线程都创建一个SimpleDateFormat对象。那么为什么SimpleDateFormat会有线程安全问题呢?已经如何重现SimpleDateFormat线程安全问题和如何解决将是本文的重点。
SimpleDateFormat产生线程不安全的原因
我们从java的源代码开始,查看SimpleDateFormat和DateFormat对parse方法的实现部分。整个方法的代码片段翻译一下大致功能如下:
Date parse() { calendar.clear(); // 清理calendar ... // 执行一些操作, 设置 calendar 的日期什么的 calendar.getTime(); // 获取calendar的时间 }
SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的.这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用,这里会导致的问题就是, 如果 线程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。
重现SimpleDateFormat线程安全问题
下面我们将通过以下代码对SimpleDateFormat线程安全的问题进行重现:
public class DateFormatTest extends Thread { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); //:www.xttblog.com 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(); } }
使用Debug模式执行这段代码,并在sdf.parse()方法里打上断点,操作步骤如下:
- 首先A线程跑起来以后会进入sleep
- B线程跑起来, 卡在断点处
- A线程醒过来, 执行 calendar.clear(), 并将设置sdf.calendar的date为1991-09-13, 此时 A B 的 calendar 都为 1991-09-13
- 让断点继续执行, 输出如下
A : date: Fri Sep 13 00:00:00 CDT 1991 B : date: Fri Sep 13 00:00:00 CDT 1991
上面的结果可以说明一切了,当然这样的测试可能还不够明显,下面我们再来一段代码进行测试,如下:
@Test public void testUnThreadSafe() throws Exception { final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,S"); //:www.xttblog.com final String[] dateStrings = { "2014-04-30 18:51:01,61", "2014-04-30 18:51:01,461", "2014-04-30 18:51:01,361", "2014-04-30 18:51:01,261", "2014-04-30 18:51:01,161", }; int threadNum = 5; Thread[] parseThreads = new Thread[threadNum]; for (int i=0; i<threadNum; i++) { parseThreads[i] = new Thread(new Runnable() { public void run() { for (int j=0; j<dateStrings.length; j++) { try { System.out.println(Thread.currentThread().getName() + " " + sdf.parse(dateStrings[j])); } catch (ParseException e) { e.printStackTrace(); } } } }); parseThreads[i].start(); } for (int i=0; i<threadNum; i++) { parseThreads[i].join(); } }
执行这个方法,将会抛出异常:java.lang.NumberFormatException: multiple points
SimpleDateFormat线程安全问题的解决办法
我将解决方案总结为以下几种:
- 对SimpleDateFormat实例的相关部分代码进行加锁处理
- SimpleDateFormat实例不要使用static修饰,也不要使用全局变量
- 使用ThreadLocal
- 每次都new一个SimpleDateFormat实例
在并发情况下,网站的请求任务与线程执行情况大概可以理解为如下:
如果在并发请求很高的时候,我们就需要特别注意了,上面的第三种方法是我推荐的使用方式。其他的做法不是不彻底就是太消耗性能。比如每一个线程都new一个SimpleDateFormat,太浪费资源了。因此,我建议使用ThreadLocal创建一个对单个线程来说全局的变量,保证线程安全,当然可以使用第三方工具类如Apache commons 里的FastDateFormat或者Joda-Time类库来处理。
版权声明:本文为博主原创文章,未经博主允许不得转载。
: » 从SimpleDateFormat的实现原理讲线程安全问题与解决方案
原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/251476.html