SimpleDateFormat是Java提供的一个格式化和解析日期的工具类
但是由于它是线程不安全的,多线程共用一个SimpleDateFormat实例对日期进行解析或者格式化会导致程序出错



问题重现

public class TestSimpleDateFormat {
     //(1)创建单例实例
     static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
     public static void main(String[] args) {
         //(2)创建多个线程,并启动
         for (int i = 0; i <100 ; ++i) {
             Thread thread = new Thread(new Runnable() {
                 public void run() {
                     try {//(3)使用单例日期实例解析文本
                         System.out.println(sdf.parse("2017-12-13 15:17:27"));
                     } catch (ParseException e) {
                         e.printStackTrace();
                     }
                 }
             });
             thread.start();//(4)启动线程
         }
     }
 }

代码(1)创建了SimpleDateFormat的一个实例,
代码(2)创建100个线程,每个线程都公用同一个sdf对象对文本日期进行解析,
多运行几次就会抛出java.lang.NumberFormatException异常,
加大线程的个数有利于该问题复现。



问题分析

SimpleDateFormat实例里面有一个Calendar对象
SimpleDateFormat之所以是线程不安全的就是因为Calendar是线程不安全的
而Calendar之所以是线程不安全的是因为其中存放日期数据的变量都是线程不安全的,比如里面的fields,time等



解决方案

第一种方式:每次使用时候new一个SimpleDateFormat的实例,这样可以保证每个实例使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其它引用,就会需要被回收,开销会很大。
第二种方式:可以使用synchronized进行同步,但使用同步意味着多个线程要竞争锁,在高并发场景下会导致系统响应性能下降。

Thread thread = new Thread(new Runnable() {
     public void run() {
         try {// (3)使用单例日期实例解析文本
             synchronized (sdf) {
                 System.out.println(sdf.parse("2017-12-13 15:17:27"));
             }
         } catch (ParseException e) {
             e.printStackTrace();
         }
     }
 });

第三种方式:使用ThreadLocal,这样每个线程只需要使用一个SimpleDateFormat实例相比第一种方式大大节省了对象的创建销毁开销,并且不需要对多个线程直接进行同步,使用ThreadLocal方式代码如下:

public class TestSimpleDateFormat2 {
     // (1)创建threadlocal实例
     static ThreadLocal<DateFormat> safeSdf = new ThreadLocal<DateFormat>(){
         @Override 
         protected SimpleDateFormat initialValue(){
             return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         }
     };
     
     public static void main(String[] args) {
         // (2)创建多个线程,并启动
         for (int i = 0; i < 10; ++i) {
             Thread thread = new Thread(new Runnable() {
                 public void run() {
                     try {// (3)使用单例日期实例解析文本
                             System.out.println(safeSdf.get().parse("2017-12-13 15:17:27"));
                     } catch (ParseException e) {
                         e.printStackTrace();
                     }
                 }
             });
             thread.start();// (4)启动线程
         }
     }
 }

代码(1)创建了一个线程安全的SimpleDateFormat实例,步骤(3)在使用的时候首先使用get()方法获取当前线程下SimpleDateFormat的实例,在第一次调用ThreadLocal的get()方法适合会触发其initialValue方法用来创建当前线程所需要的SimpleDateFormat对象。



总结

SimpleDateFormat是线程不安全的,应该避免多线程下使用SimpleDateFormat的单个实例,多线程下使用时候最好使用ThreadLocal对象