java中SimpleDateFormat线程不安全原因及解决方案

先说结论,在java中SimpleDateFormat日期格式对象是非线程安全的,如果把SimpleDateFormat对象用static关键字修饰,那么在多线程中使用这同一个对象,是有可能会出错的。

一、代码演示

下面用代码演示一下线程不安全的情况:

public class SimpleDataFormatTest {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        int cpuNum = 2;// Runtime.getRuntime().availableProcessors();
        CountDownLatch latch = new CountDownLatch(cpuNum);
        for (int i = 0; i < cpuNum; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MICROSECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    latch.await();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                String beforeDateStr = sdf.format(new Date());
                Date date = null;
                try {
                    date = sdf.parse(beforeDateStr);
                } catch (ParseException e) {
                    e.printStackTrace();
                    return;
                }
                String afterDateStr = sdf.format(date);
                System.out.println(Thread.currentThread().getName() + "-->beforeDateStr=" + beforeDateStr
                    + ",afterDateStr=" + afterDateStr);
            }).start();
            latch.countDown();
        }
    }

}

运行结果:

  • 第一种情况:正常运行,且数据正确
Thread-0-->beforeDateStr=2020-10-28 17:04:55,afterDateStr=2020-10-28 17:04:55
Thread-1-->beforeDateStr=2020-10-28 17:04:55,afterDateStr=2020-10-28 17:04:55
  • 第二种情况:正常运行,但数据出错
Thread-1-->beforeDateStr=2020-10-28 17:05:51,afterDateStr=2020-10-28 00:00:00
Thread-0-->beforeDateStr=2020-10-28 17:05:51,afterDateStr=1101-05-30 15:05:51
  • 第三种情况:程序抛出异常
Exception in thread "Thread-1" Thread-0-->beforeDateStr=2020-10-28 17:05:30,afterDateStr=2113-08-28 17:05:30
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at learnbymaven.datetime.SimpleDataFormatTest.lambda$0(SimpleDataFormatTest.java:35)
	at java.lang.Thread.run(Thread.java:748)

把上面的程序多运行几次,这三种情况都会重现出来。

二、线程不安全原因

那么为什么会出现这种情况呢,看一下SimpleDateFormat的部分源码:

  1. 继承了父类的成员变量
 protected Calendar calendar;
  1. 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;
    }

可以看到,format方法中,调用了 calendar.setTime(date)代码设置时间,由于SimpleDateFormat对象使用static关键字修饰,此时calendar也相当于是共享变量了,因此在多线程环境下,当多个线程同时使用相同的SimpleDateFormat对象的话,调用format方法时,多个线程会同时调用calender.setTime方法,导致time被别的线程修改,因此线程是不安全的。

同时,parse方法也不是线程安全的

三、解决方法

下面例举一下常见的解决方法:

  1. 将SimpleDateFormat定义成局部变量,每次调用的时候都实例化一个对象,不过这样会比较浪费内存。
	 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	 String beforeDateStr = sdf.format(new Date());
  1. 在调用SimpleDateFormat对象的地方加同步锁synchronized,保证同一时刻,只有一个线程可以访问该对象,不过这样性能会很差。
	String beforeDateStr = null;
	String afterDateStr = null;
	synchronized (SimpleDateFormat.class) {
	    beforeDateStr = sdf.format(new Date());
	    Date date = null;
	    try {
	        date = sdf.parse(beforeDateStr);
	    } catch (ParseException e) {
	        e.printStackTrace();
	        return;
	    }
	    afterDateStr = sdf.format(date);
	}
	
	System.out.println(Thread.currentThread().getName() + "-->beforeDateStr=" + beforeDateStr
	    + ",afterDateStr=" + afterDateStr);
  1. 使用ThreadLocal,让每个线程拥有自己的SimpleDateFormat对象。
public class SimpleDataFormatTest {

    private static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();

    public static void main(String[] args) {
        int cpuNum = 2;// Runtime.getRuntime().availableProcessors();
        CountDownLatch latch = new CountDownLatch(cpuNum);
        for (int i = 0; i < cpuNum; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MICROSECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    latch.await();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                SimpleDateFormat sdf = getSimpleDateFormat();
                String beforeDateStr = sdf.format(new Date());
                Date date = null;
                try {
                    date = sdf.parse(beforeDateStr);
                } catch (ParseException e) {
                    e.printStackTrace();
                    return;
                }
                String afterDateStr = sdf.format(date);

                System.out.println(Thread.currentThread().getName() + "-->beforeDateStr=" + beforeDateStr
                    + ",afterDateStr=" + afterDateStr);
            }).start();
            latch.countDown();
        }
    }

    private static SimpleDateFormat getSimpleDateFormat() {
        if (tl.get() == null) {
            tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        }
        // 为了演示效果,保证SimpleDateFormat对象每次都是从ThreadLocal中获取的
        return tl.get();
    }

}
  1. 使用第三方的时间类库,或者jdk8的新日期API,如使用DateTimeFormatter代替SimpleDateFormat。
public class SimpleDataFormatTest {

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

    public static void main(String[] args) {
        int cpuNum = Runtime.getRuntime().availableProcessors();
        CountDownLatch latch = new CountDownLatch(cpuNum);
        for (int i = 0; i < cpuNum; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MICROSECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    latch.await();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }

                String beforeDateStr = LocalDateTime.now().format(df);
                LocalDateTime date = null;
                date = LocalDateTime.parse(beforeDateStr, df);
                String afterDateStr = date.format(df);

                System.out.println(Thread.currentThread().getName() + "-->beforeDateStr=" + beforeDateStr
                    + ",afterDateStr=" + afterDateStr);
            }).start();
            latch.countDown();
        }
    }

}

你可能感兴趣的:(java,DateFormat,线程安全,java,后端)