为什么禁止把SimpleDateFormat定义为static类型的?

一、前言

最近使用Sonarqube扫描代码的时候发现一个问题,使用static修饰SimpleDateFormat的实例被标记为了Bug

Sonarqube中的提示信息如下:

并不是标准Java库中的所有类都被编写为线程安全的。以多线程方式使用它们很可能在运行时导致数据问题或异常

当Calendar, DateFormat, javax.xml.xpath.XPath, 或者 javax.xml.validation.SchemaFactory的实例被标记为静态时会出现问题

  • 不合规的代码示例:
public class MyClass {
	private static SimpleDateFormat format = new SimpleDateFormat("HH-mm-ss");
	private static Calendar calendar = Calendar.getInstance();
  • 合规的代码实例:
public class MyClass {
	private SimpleDateFormat format = new SimpleDateFormat("HH-mm-ss");
	private Calendar calendar = Calendar.getInstance();

又查看了一下《阿里巴巴开发手册》中对SimpleDateFormat是怎么对待的

在这里插入图片描述

其实主要的问题在于SimpleDateFormat并不是一个线程安全的类

二、问题场景复现

在多线程环境下:

public class SimpleDateFormatTest {
	private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

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

	public static Date parse(String strDate) throws ParseException {
		return sdf.parse(strDate);
	}

	public static void main(String[] args) throws InterruptedException {
		ExecutorService service = Executors.newFixedThreadPool(100);
		for (int i = 0; i < 20; i++) {
			service.execute(() -> {
				try {
					System.out.println(parse("2019-04-03 17:45:00"));
				} catch (ParseException e) {
					e.printStackTrace();
				}

			});
		}
		service.shutdown();
		service.awaitTermination(1, TimeUnit.DAYS);
	}
}

输出信息如下:

Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-3" 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.hand.thread.SimpleDateFormatTest.parse(SimpleDateFormatTest.java:18)
	at com.hand.thread.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:26)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
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.hand.thread.SimpleDateFormatTest.parse(SimpleDateFormatTest.java:18)
	at com.hand.thread.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:26)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Fri Apr 03 17:45:00 CST 2201
Wed Apr 03 17:45:00 CST 2019
Fri Apr 03 17:45:00 CST 2201
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Exception in thread "pool-1-thread-20" Exception in thread "pool-1-thread-19" java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)Wed Apr 03 17:45:00 CST 2019

	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.hand.thread.SimpleDateFormatTest.parse(SimpleDateFormatTest.java:18)
	at com.hand.thread.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:26)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
Wed Apr 03 17:00:00 CST 2019
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
Wed Apr 03 17:00:00 CST 2019
	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:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.hand.thread.SimpleDateFormatTest.parse(SimpleDateFormatTest.java:18)
	at com.hand.thread.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:26)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Mon Apr 02 17:45:00 CST 3

部分线程获取的时间不对,部分线程直接报java.lang.NumberFormatException: multiple points

三、多线程不安全原因

把SimpleDateFormat定义为静态变量,多线程环境下SimpleDateFormat的实例就会被多个线程共享,B线程会读取到A线程的时间,就会出现时间差异和其他各种问题

SimpleDateFormat和它继承的DateFormat都不是线程安全的

看一下SimpleDateFormat的format()方法的源码:

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

    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        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),SimpleDateFormat的format方法实际操作的就是Calendar

因为我们声明SimpleDateFormat为static变量,那么它的Calendar变量也就是一个共享变量,可以被多个线程访问

假设线程A执行完calendar.setTime(date),把时间设置成2019-01-02,这时候被挂起,线程B获得CPU执行权。线程B也执行到了calendar.setTime(date),把时间设置为2019-01-03。线程挂起,线程A继续走,calendar还会被继续使用(subFormat方法),而这时calendar用的是线程B设置的值了,而这就是引发问题的根源,出现时间不对,线程挂死等等

四、解决方案

使用JDK1.8的DateTimeFormatter

public class DateTimeFormatterTest {
	private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

	public static String formatDate(LocalDateTime date) {
		return formatter.format(date);
	}

	public static LocalDateTime parse(String dateNow) {
		return LocalDateTime.parse(dateNow, formatter);
	}

	public static void main(String[] args) throws InterruptedException, ParseException {
		ExecutorService service = Executors.newFixedThreadPool(100);
		for (int i = 0; i < 20; i++) {
			service.execute(() -> {
				for (int j = 0; j < 10; j++) {
					try {
						System.out.println(parse(formatDate(LocalDateTime.now())));
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			});
		}
		service.shutdown();
		service.awaitTermination(1, TimeUnit.DAYS);
	}
}

参考:https://542869246.github.io/2019/01/02/还在使用SimpleDateFormat?你的项目崩没?/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值