现象
由于SimpleDateFormat的线程安全问题,导致使用将其定义为成员变量时,会导致

public class SimpleDateFormatTest extends Thread{

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

    private String name;
    private String dateStr;

    public SimpleDateFormatTest(String name, String dateStr) {
        this.name = name;
        this.dateStr = dateStr;
    }

    @Override
    public void run() {

        try {
            Date date = sdf.parse(dateStr);
            System.out.println(name + " --- date --- " + date);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        executorService.execute(new SimpleDateFormatTest("A1", "20200301"));
        executorService.execute(new SimpleDateFormatTest("A2", "20200325"));
        executorService.execute(new SimpleDateFormatTest("A3", "20200327"));
        executorService.shutdown();
    }
}

null

原因
查看源码发现,SimpleDateFormat类内部有一个Calendar对象引用,它用来储存和这个SimpleDateFormat相关的日期信息,例如sdf.parse(dateStr),sdf.format(date) 诸如此类的方法参数传入的日期相关String,Date等,都是交由Calendar引用来储存的。这样就会导致一个问题,如果你的SimpleDateFormat是个static的, 那么多个thread之间就会共享这个SimpleDateFormat, 同时也是共享这个Calendar引用。单例、多线程、又有成员变量(这个变量在方法中是可以修改的),这个场景是不是很像servlet,在高并发的情况下,容易出现幻读成员变量的现象,故说SimpleDateFormat是线程不安全的对象。

解决方案
1、将SimpleDateFormat定义成局部变量。
缺点:每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。
2、方法加同步锁synchronized,在同一时刻,只有一个线程可以执行类中的某个方法。
缺点:性能较差,每次都要等待锁释放后其他线程才能进入。
3、使用java8提供的DateTimeFormatter。