SimpleDateFormat是线程安全的吗

326 阅读1分钟

我们在对日期进行格式化时,除了用第三方的工具类,SimpleDateFormat应该是用的最多的。但大部分都是在单线程的环境下,那么SimpleDateFormat是线程安全的吗?我们首先写一个测试用例。

public class TestSimpleDateFormat {
    public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static void main(String[] args) {
        for (int i=0; i<10; i++) {
            Thread thread = new Thread(() -> {
                try {
                    System.out.println(sdf.parse("2021-12-29 21:32:33"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
        }
    }
}

第一次执行并没有报错,第二次执行时结果如下:

java.lang.NumberFormatException: empty String 
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)   
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110) 
    at java.lang.Double.parseDouble(Double.java:538)

可见SimpleDateFormat不是线程安全的,在解释报错原因之前,我们先看一下SimpleDateFormat的类继承结构图:

simpledateformat-extend.png

SimpleDateFormat继承了DateFormat,DateFormat内部有一个Calendar对象的引用,主要用来存储和SimpleDateFormat相关的日期信息

SimpleDateFormat对parse()方法的实现。关键代码如下:

@Override 
public Date parse(String text, ParsePosition pos) { 
    ...省略中间代码 
    Date parsedDate; 
    try { 
        ... parsedDate = calb.establish(calendar).getTime(); 
    } catch (IllegalArgumentException e) { ... } 
    return parsedDate; 
}

establish()的实现如下:

Calendar establish(Calendar cal) { 
    ... 
    cal.clear(); 
    for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) { 
        for (int index = 0; index <= maxFieldIndex; index++) { 
            if (field[index] == stamp) { 
                cal.set(index, field[MAX_FIELD + index]); break; 
            } 
        } 
    } 
    ... 
    return cal; 
}

在多个线程共享SimpleDateFormat时,同时也共享了Calendar引用,在如上代码中,calendar首先会进行clear操作,然后进行set操作,在多线程情况下,set操作会覆盖之前的值,而且在后续对日期进行操作时,也可能会因为clear操作被清除导致异常

如何解决

  1. 将SimpleDateFormat定义成局部变量,每次使用时都new一个新对象,缺点就是频繁创建对象以及垃圾回收,影响系统性能
  2. 对方法加synchronized同步锁,缺点就是在高并发情况下性能较差
  3. 使用第三方库,比如joda-time,hutool等,将线程安全问题转移给第三方
  4. 使用ThreadLocal

使用ThreadLocal代码示例如下:

public class TestSimpleDateFormat {
    public static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(
            () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );
    public static void main(String[] args) {
        for (int i=0; i<10; i++) {
            Thread thread = new Thread(() -> {
                try {
                    System.out.println(threadLocal.get().parse("2021-12-29 21:32:33"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
        }
    }
}

扫码_搜索联合传播样式-标准色版.png