从SimpleDateFormat的实现原理讲线程安全问题与解决方案

JAVA herman 2083浏览 0评论
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog,发送下载链接帮助你免费下载!
本博客日IP超过1800,PV 2600 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog,之前的微信号好友位已满,备注:返现
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

部分程序员可能都遇到过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()方法里打上断点,操作步骤如下:

  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

上面的结果可以说明一切了,当然这样的测试可能还不够明显,下面我们再来一段代码进行测试,如下:

@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线程安全问题的解决办法

我将解决方案总结为以下几种:

  1. 对SimpleDateFormat实例的相关部分代码进行加锁处理
  2. SimpleDateFormat实例不要使用static修饰,也不要使用全局变量
  3. 使用ThreadLocal
  4. 每次都new一个SimpleDateFormat实例

在并发情况下,网站的请求任务与线程执行情况大概可以理解为如下:

网站的并发请求实例

如果在并发请求很高的时候,我们就需要特别注意了,上面的第三种方法是我推荐的使用方式。其他的做法不是不彻底就是太消耗性能。比如每一个线程都new一个SimpleDateFormat,太浪费资源了。因此,我建议使用ThreadLocal创建一个对单个线程来说全局的变量,保证线程安全,当然可以使用第三方工具类如Apache commons 里的FastDateFormat或者Joda-Time类库来处理。

版权声明:本文为博主原创文章,未经博主允许不得转载。

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加QQ1群:135430763(2000人群已满),QQ2群:454796847(已满),QQ3群:187424846(已满)。QQ群进群密码:xttblog,想加微信群的朋友,之前的微信号好友已满,请加博主新的微信号:xttblog,备注:“xttblog”,添加博主微信拉你进群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作可添加助理微信进行沟通!

本文原文出处:业余草: » 从SimpleDateFormat的实现原理讲线程安全问题与解决方案