SimpleDateFormat线程安全问题

1.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.

在 SimpleDateFormat 类的 JavaDoc 中,描述了该类不能够保证线程安全,建议为每个线程创建单独的日期/时间格式实例,如果多个线程同时访问一个日期/时间格式,它必须在外部进行同步。那么在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题 。

1、每个线程创建单独的日期/时间格式实例

大量的创建 SimpleDateFormat 实例对象,然后再丢弃这个对象,占用大量的内存和 JVM 空间。

2、创建一个静态的 SimpleDateFormat 实例,在使用时直接使用这个实例进行操作

private static final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date();
df.format(date);

这个方法在大部分的时间里面都会工作得很好,但一旦在生产环境中一定负载情况下时,这个问题就出来了。他会出现各种不同的情况,比如转化的时间不正确,比如报错,比如线程被挂死等等 。

测试

public class TestSimpleDateFormat {

    public final static SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(Date date){
        return TIME_FORMAT.format(date);
    }

    public static Date parse(String date) throws ParseException {
        return TIME_FORMAT.parse(date);
    }

    public static class TestSimpleDateFormatThreadSafe extends Thread{
        @Override
        public void run() {
            while (true) {
                try {
                    this.join(2000);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
//                System.out.println(this.getName() + ":" + format(new Date()));
                try {
                    System.out.println(this.getName() + ":" + parse("2018-06-20 01:18:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args){
        for (int i=0;i<3;i++){
            new TestSimpleDateFormatThreadSafe().start();
        }
    }
}

result

Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    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.example.utils.TestSimpleDateFormat.parse(TestSimpleDateFormat.java:43)
    at com.example.utils.TestSimpleDateFormat$TestSimpleDateFormatThreadSafe.run(TestSimpleDateFormat.java:65)
Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    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.example.utils.TestSimpleDateFormat.parse(TestSimpleDateFormat.java:43)
    at com.example.utils.TestSimpleDateFormat$TestSimpleDateFormatThreadSafe.run(TestSimpleDateFormat.java:65)
Thread-2:Wed Jun 20 01:18:20 CST 2018

2.为什么会这样

通过看 JDK 源码来看看为什么 SimpleDateFormat 和 DateFormat 类不是线程安全的真正原因:

SimpleDateFormat 继承了 DateFormat,在 DateFormat 中定义了一个 protected 属性的 Calendar 类的对象:calendar。JDK 的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误。

在 SimpleDateFormat 中的 format 方法源码中:

@Override
    public StringBuffer format(Date date, StringBuffer toAppendTo,
                               FieldPosition pos)
    {
        pos.beginIndex = pos.endIndex = 0;
        return format(date, toAppendTo, pos.getFieldDelegate());
    }

    // 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) 这条语句改变了 calendar,在 subFormat 方法里,calendar 还会用到,而这就是引发问题的根源。在一个多线程环境下,有两个线程持有了同一个 SimpleDateFormat 的实例,分别调用format 方法:

线程 1 调用 format 方法,改变了 calendar 这个字段。
线程 1 中断了。
线程 2 开始执行,它也改变了 calendar。
线程 2 中断了。
线程 1 回来了

此时,calendar 已然不是它所设的值,而是走上了线程 2 设计的道路。如果多个线程同时争抢 calendar 对象,则会出现各种问题,时间不对,线程挂死等等。

分析一下 format 的实现,我们不难发现,用到成员变量 calendar,唯一的好处,就是在调用 subFormat 时,少了一个参数,却带来了许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。

这个问题背后隐藏着一个更为重要的问题–无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format 方法在运行过程中改动了 SimpleDateFormat 的 calendar 字段,所以,它是有状态的。

这也同时提醒我们在开发和设计系统的时候注意下一下三点:

1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明

2.多线程环境下,对每一个共享的可变变量都要注意其线程安全性

3.我们的类和方法在做设计的时候,要尽量设计成无状态的

3.解决办法

1、用的时候创建实例对象

在需要用到 SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。

2、使用同步:同步 SimpleDateFormat 对象

public static String format(Date date){
        synchronized(TIME_FORMAT){
            return TIME_FORMAT.format(date);
        }
    }

    public static Date parse(String date) throws ParseException {
        synchronized(TIME_FORMAT){
            return TIME_FORMAT.parse(date);
        }
    }

注:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要 block 等待,多线程并发量大的时候会对性能有一定的影响。

3、使用 ThreadLocal

private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

public static String format(Date date){
        return threadLocal.get().format(date);
    }

    public static Date parse(String date) throws ParseException {
        return threadLocal.get().parse(date);
    }

说明:使用 ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。

对比第一种方式,此方式创建的DateFormat对象为线程个数,对于方式1而言,每个线程可能会有多个DateFormat对象。

4、java8中使用DateTimeFormatter

Java 8 提供了新的日期时间 API,其中包括用于日期时间格式化的 DateTimeFormatter,

DateTimeFormatter 是线程安全的,而 SimpleDateFormat 并不是线程安全。

 public final static DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

 public static String format(LocalDate date){
        return date.format(TIME_FORMAT);
    }

    public static LocalDate parse(String date) throws ParseException {
        return LocalDate.parse(date,TIME_FORMAT);
    }

4.Finally

SimpleDateFormat 是线程不安全的类,多线程环境下注意线程安全问题。

如果是 Java 8 ,建议使用 DateTimeFormatter 代替 SimpleDateFormat。


   转载规则


《SimpleDateFormat线程安全问题》 yywzt 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录