多线程环境下SimpleDateFormat线程不安全问题

1.单线程环境下SimpleDateFormat去解析时间并没有什么问题。

2.但是在多线程情况下,会出现异常。下面我们就来分析分析SimpleDateFormat为什么不安全?是怎么引发的?以及多线程下有那些SimpleDateFormat的解决方案?

3.问题场景复现

  • 一般我们使用SimpleDateFormat的时候会把它定义为一个静态变量,避免频繁创建它的对象实例,如下代码:
  • public class SimpleDateFormatTest {
    
        private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        public static String formatDate(Date date) throws ParseException {
            return sdf.format(date);
        }
    
        public static Date parse(String strDate) throws ParseException {
            return sdf.parse(strDate);
        }
    
        public static void main(String[] args) throws InterruptedException, ParseException {
    
            System.out.println(sdf.format(new Date()));
            
        }
    }
  • 是不是感觉没什么毛病?单线程下自然没毛病了,都是运用到多线程下就有大问题了。
  • 测试下:
  • public static void main(String[] args) throws InterruptedException, ParseException {
    
        ExecutorService service = Executors.newFixedThreadPool(100);
    
        for (int i = 0; i < 20; i++) {
            service.execute(() -> {
                for (int j = 0; j < 10; j++) {
                    try {
                        System.out.println(parse("2018-01-02 09:45:59"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        // 等待上述的线程执行完
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);
    }
  • 控制台打印结果:
  • Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
        at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
        at java.lang.Double.parseDouble(Double.java:510)
        at java.text.DigitList.getDouble(DigitList.java:151)
        at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
        at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
        at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
        at java.text.DateFormat.parse(DateFormat.java:335)
        at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
        at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
    Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
        at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
        at java.lang.Double.parseDouble(Double.java:510)
        at java.text.DigitList.getDouble(DigitList.java:151)
        at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
        at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
        at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
        at java.text.DateFormat.parse(DateFormat.java:335)
        at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
        at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
    Thread-2:Mon May 24 06:02:20 CST 2021
    Thread-2:Fri May 24 06:02:20 CST 2013
    Thread-2:Fri May 24 06:02:20 CST 2013
    Thread-2:Fri May 24 06:02:20 CST 2013

     

  • 你看这不崩了?部分线程获取的时间不对,部分线程直接报java.lang.NumberFormatException: multiple points错,线程直接挂死了。

4.多线程不安全原因

  • 因为我们吧SimpleDateFormat定义为静态变量,那么多线程下SimpleDateFormat的实例就会被多个线程共享,B线程会读取到A线程的时间,就会出现时间差异和其它各种问题。
  • SimpleDateFormat和它继承的DateFormat类也不是线程安全的
  • 那到底是为什么呢?
    • 来看看SimpleDateForma中t的format()方法的源码
  • // Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.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;
    }

 

  • 注意calendar.setTime(date);,SimpleDateFormat的format方法实际操作的就是Calendar(Calendar变量也就是一个共享变量线程不安全)。也正是因为每次在转化时间的时候foramat会先把时间set到calendar中,这样就会导致A线程读取到B线程的时间
  • 因为我们声明SimpleDateFormat为static变量,那么它的Calendar变量也就是一个共享变量,可以被多个线程访问。
  • 假设线程A执行完calendar.setTime(date),把时间设置成2019-01-02,这时候被挂起,线程B获得CPU执行权。线程B也执行到了calendar.setTime(date),把时间设置为2019-01-03。线程挂起,线程A继续走,calendar还会被继续使用(subFormat方法)刚才set到calendar中的时间,而这时calendar用的是线程B设置的值了,而这就是引发问题的根源,出现时间不对,线程挂死等等。
  • 其实SimpleDateFormat源码上作者也给过我们提示:
  • * 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.
    日期格式不同步。 
    建议为每个线程创建单独的格式实例。
    如果多个线程同时访问一种格式,则必须在外部同步该格式。

 

5.解决方案

方法一:每次只在需要的时候创建新实例,不用static修饰()

public static String formatDate(Date date) throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    return sdf.format(date);
}

public static Date parse(String strDate) throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    return sdf.parse(strDate);
}
  • 如上代码,仅在需要用到的地方创建一个新的实例,就没有线程安全问题,不过也加重了创建对象的负担,会频繁地创建和销毁对象,效率较低。

方法二:使用synchronized

  • private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    public static String formatDate(Date date) throws ParseException {
        synchronized(sdf){
            return sdf.format(date);
        }
    }
    
    public static Date parse(String strDate) throws ParseException {
        synchronized(sdf){
            return sdf.parse(strDate);
        }
    }
  • 简单粗暴,synchronized往上一套也可以解决线程安全问题,缺点自然就是并发量大的时候会对性能有影响,线程阻塞。

方法三:使用本地线程ThreadLocal(推荐使用)

  • private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    
    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }
    
    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
  • ThreadLocal可以确保每个线程都可以得到单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。

《阿里巴巴开发手册》中也是如此使用的:

注:可以先了解一下ThreadLocal是干嘛用的,都有哪些api,附链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值