为何不用SimpleDateFormat?-- SimpleDateFormat非线性安全性简要分析

前言:每个Java项目,一般都会有一些工具类,其中的日期工具类,往往很容易看到SimpleDateFormat这个类的使用。一般我们是拿来做日期的格式化的,但是你有没有想过,这个类的一些方法是非线性安全的。

一. 案例

我们用static去修饰SimpleDateFormat,代码里启动了10个线程,都用Test类下的simpleDateFormat 属性去做日期格式化操作。

public class Test {
    public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    public static String[] dateStringArray = new String[]{"2021-01-01", "2021-01-02",
            "2021-01-03", "2021-01-04", "2021-01-05", "2021-01-06",
            "2021-01-07", "2021-01-08", "2021-01-09", "2021-01-10"};

    @Test
    public void testSimpleDateFormat() {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    String formatDate = simpleDateFormat.format(simpleDateFormat.parse(dateStringArray[finalI]));
                    if (!formatDate.equals(dateStringArray[finalI])) {
                        System.out.println(Thread.currentThread().getName() + "日期格式化出现问题!原日期:" + dateStringArray[finalI] + ",转换后日期:" + formatDate);
                    }
                } catch (ParseException e) {
                    System.out.println(e);
                }
            }, "线程" + i).start();
        }
    }
}

看看结果会发生什么(部分截图):
在这里插入图片描述
我们可以从该案例中得出以下结论:

  • 多线程情况下使用同一个SimpleDateFormat对象进行日期格式化可能会出现日期转换错误。
  • SimpleDateFormat是非线性安全的。

二. 源码分析

首先,上述代码当中,核心的代码无非就是两个:

  1. SimpleDateFormat.format()
  2. SimpleDateFormat.parse()

那么我们来以format方法为例,在这里我们只关注非线性安全的原因,而不去深入探究format的运行机制。

1.点进方法,可以看见首先进入的是DateFormat这个抽象类:

public abstract class DateFormat extends Format {
	protected Calendar calendar;
	
	public final String format(Date date){
        return format(date, new StringBuffer(), DontCareFieldPosition.INSTANCE).toString();
    }
    
	public abstract StringBuffer format(Date date, StringBuffer toAppendTo,
	                                        FieldPosition fieldPosition);
	}

calendar这个属性,其注释的意思是:

用于计算时间的实例。该字段用于格式化和解析。

2.上述format方法会调用当前类的抽象format方法,最终实现调用的是SimpleDateFormat子类下的format方法。

public class SimpleDateFormat extends DateFormat {
	// 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;
    }
}

我们发现,最终调用到的SimpleDateFormat.format方法,里面会先执行calendar.setTime(date);,而这个calendar在父类DateFormat中被定义,但是我们可以看到,这个变量是一个共享变量,并且这个共享变量没有做线程安全控制。

2.1 共享变量

首先大家可以看下我写的这篇文章深入理解Java虚拟机系列(四)–Java内存模型和线程

我在这里将重要的内容贴出来:

Java内存模型规定:

  1. 所有的变量都存储在主内存中
  2. 每条线程有属于自己的工作内存,其中保存了被该线程使用到的变量的主内存副本拷贝
  3. 线程对变量的所有操作都必须在工作内存当中进行,而不能直接读写主内存中的变量
  4. 不同的线程之间无法直接访问对方工作内存中的变量(私有),线程间变量值的传递需要通过主内存来完成。

在这里插入图片描述

那么简而言之,共享变量也就是所有线程可以进行访问和修改的这么一个变量。

那么,对于calendar.setTime(date);这段代码而言,由于calendar变量并没有做线程安全处理,因此在高并发情况下,可能会出现这种情况:

  1. 线程A调用calendar.setTime(D1);
  2. 线程B调用calendar.setTime(D2);的时候,就会覆盖这个calendar变量的值。
  3. 若线程A还没有跑后面的内容,那么此时此刻,对于线程A而言,明明是希望转换D1的值,却输出了D2的值。
  4. 因此就产生了线性安全问题。

三. 解决方案(依旧使用SimpleDateFormat)

上述问题的本质,也就是多个线程同时访问并修改了同一个对象的成员,导致出现了线性安全问题。那么我们可以这么修改代码:

3.1 每个线程都创建一个SimpleDateFormat

每个线程去创建属于自己的SimpleDateFormat对象。

public class Test6 {
    public  SimpleDateFormat simpleDateFormat;
    public static String[] dateStringArray = new String[]{"2021-01-01", "2021-01-02",
            "2021-01-03", "2021-01-04", "2021-01-05", "2021-01-06",
            "2021-01-07", "2021-01-08", "2021-01-09", "2021-01-10"};

    @Test
    public void testSimpleDateFormat() {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
                    String formatDate = simpleDateFormat.format(simpleDateFormat.parse(dateStringArray[finalI]));
                    if (!formatDate.equals(dateStringArray[finalI])) {
                        System.out.println(Thread.currentThread().getName() + "日期格式化出现问题!原日期:" + dateStringArray[finalI] + ",转换后日期:" + formatDate);
                    }
                } catch (ParseException e) {
                    System.out.println(e);
                }
            }, "线程" + i).start();
        }
    }
}

代码执行后,则并不会出现同样的问题:
在这里插入图片描述

3.2 使用ThreadLocal

public class Test6 {
    public static ThreadLocal<SimpleDateFormat> t = new ThreadLocal<SimpleDateFormat>();
    public static String[] dateStringArray = new String[]{"2021-01-01", "2021-01-02",
            "2021-01-03", "2021-01-04", "2021-01-05", "2021-01-06",
            "2021-01-07", "2021-01-08", "2021-01-09", "2021-01-10"};

    public static SimpleDateFormat getSimpleDateFormat() {
        SimpleDateFormat res;
        res = t.get();
        if (res == null) {
            res = new SimpleDateFormat();
            t.set(res);
        }
        return res;
    }

    @Test
    public void testSimpleDateFormat() {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    SimpleDateFormat simpleDateFormat = getSimpleDateFormat();
                    String formatDate = simpleDateFormat.format(simpleDateFormat.parse(dateStringArray[finalI]));
                    if (!formatDate.equals(dateStringArray[finalI])) {
                        System.out.println(Thread.currentThread().getName() + "日期格式化出现问题!原日期:" + dateStringArray[finalI] + ",转换后日期:" + formatDate);
                    }
                } catch (ParseException e) {
                    System.out.println(e);
                }
            }, "线程" + i).start();
        }
    }
}

后续

本篇文章还是针对于SimpleDateFormat来讲的,并且只是针对SimpleDateFormat来给出两种解决方案。问题是SimpleDateFormat并不是日期格式化的一个很好选择,阿里开发规范中明确指出,禁止使用带static修饰的SimpleDateFormat变量。

那么如果不使用SimpleDateFormat,我们如何进行日期的格式化相关操作呢?

我们可以通过LocalDateTimeLocalDateLocalTime来进行代替。他们是jdk8引入的新类,准备在下一篇文章中介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Zong_0915

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

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

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

打赏作者

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

抵扣说明:

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

余额充值