为什么SimpleDateFormat不是线程安全的?以及解决方法说明

76 篇文章 2 订阅
16 篇文章 0 订阅

一:概述

SimpleDateFormat 类主要负责日期的转换与格式化等操作,在多线程的环境中,使用此类容易造成数据转换及处理的不正确,因为 SimpleDateFormat 类并不是线程安全的,但在单线程环境下是没有问题的。

SimpleDateFormat 在类注释中也提醒大家不适用于多线程场景:

 

 

说的很清楚,SimpleDateFormat 不是线程安全的,多线程下需要为每个线程创建不同的实例。

不安全的原因是因为使用了 Calendar 这个全局变量:

 

在日期格式化的时候:

 

 

二:线程不安全的原因

我们把重点放在 calendar ,这个 format 方法在执行过程中,会操作成员变量 calendar 来保存时间 calendar.setTime(date) 。

但由于在声明 SimpleDateFormat 的时候,使用的是 static 定义的,那么这个 SimpleDateFormat 就是一个共享变量,SimpleDateFormat 中的 calendar 也就可以被多个线程访问到,所以问题就出现了,举个例子:

假设线程 A 刚执行完 calendar.setTime(date) 语句,把时间设置为 2020-09-01,但线程还没执行完,线程 B 又执行了 calendar.setTime(date) 语句,把时间设置为 2020-09-02,这个时候就出现幻读了,线程 A 继续执行下去的时候,拿到的 calendar.getTime 得到的时间就是线程B改过之后的。

除了 format() 方法以外,SimpleDateFormat 的 parse 方法也有同样的问题。

至此,我们发现了 SimpleDateFormat 的弊端,所以为了解决这个问题就是不要把 SimpleDateFormat 当做一个共享变量来使用

 

阿里巴巴 java 开发规范是怎么描述 SimpleDateFormat 的:

 

 

 

三、模拟线程安全问题

无码无真相,接下来我们创建一个线程来模拟 SimpleDateFormat 线程安全问题:

创建 TestThread.java 类:

 

结果:

从控制台打印的结果来看,使用单例的 SimpleDateFormat 类在多线程的环境中处理日期转换,极易出现转换异常(java.lang.NumberFormatException:multiple points)以及转换错误的情况。

代码:

public class TestThread extends Thread {

    private SimpleDateFormat simpleDateFormat;
    // 要转换的日期字符串
    private String dateString;

    public TestThread(SimpleDateFormat simpleDateFormat, String dateString) {
        this.simpleDateFormat = simpleDateFormat;
        this.dateString = dateString;
    }

    @Override
    public void run() {
        try {
            Date date = simpleDateFormat.parse(dateString);
            String newDate = simpleDateFormat.format(date).toString();
            if (!newDate.equals(dateString)) {
                System.out.println("ThreadName=" + this.getName()
                        + " 报错了,日期字符串:" + dateString
                        + " 转换成的日期为:" + newDate);
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}




 // 一般我们使用SimpleDateFormat的时候会把它定义为一个静态变量,避免频繁创建它的对象实例
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd");

    public static void main(String[] args) {

        String[] dateStringArray = new String[]{"2021-03-10", "2021-05-1", "2021-05-19", "2021-09-17"};

        TestThread[] threads = new TestThread[4];

        // 创建线程
        for (int i = 0; i < 4; i++) {
            threads[i] = new TestThread(simpleDateFormat, dateStringArray[i]);
        }

        // 启动线程
        for (int i = 0; i < 4; i++) {
            threads[i].start();
        }
    }

 

 

五、如何解决线程安全

1、每次使用就创建一个新的 SimpleDateFormat

创建全局工具类 DateUtils.java

 

public class DateUtils {
    public static Date parse(String formatPattern, String dateString) throws ParseException {
        return new SimpleDateFormat(formatPattern).parse(dateString);
    }

    public static String  format(String formatPattern, Date date){
        return new SimpleDateFormat(formatPattern).format(date);
    }
}

 

 2:加synchronized 锁

简单粗暴,synchronized 往上一套也可以解决线程安全问题,缺点自然就是并发量大的时候会对性能有影响,因为使用了 synchronized 加锁后的多线程就相当于串行,线程阻塞,执行效率低

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

    public static Date parse(String formatPattern, String dateString) throws ParseException {
        synchronized (simpleDateFormat){
            return simpleDateFormat.parse(dateString);
        }
    }

    public static String format(String formatPattern, Date date) {
        synchronized (simpleDateFormat){
            return simpleDateFormat.format(date);
        }
    }
}

 

3、使用ThreadLocal

ThreadLocal 是 java 里一种特殊的变量,ThreadLocal 提供了线程本地的实例,它与普通变量的区别在于,每个使用该线程变量的线程都会初始化一个完全独立的实例副本。

hreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,那么就不会存在竞争问题。

 

public class DateUtilsThreadLocal {
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };

    public static Date parse(String formatPattern, String dateString) throws ParseException {
        return threadLocal.get().parse(dateString);
    }

    public static String format(String formatPattern, Date date) {
        return threadLocal.get().format(date);
    }
}

4:推荐写法 

上边提到的阿里巴巴 java 开发手册给出了说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

日期转换,SimpleDateFormat 固然好用,但是现在我们已经有了更好地选择,Java 8 引入了新的日期时间 API,并引入了线程安全的日期类,一起来看看。

Instant:瞬时实例。
LocalDate:本地日期,不包含具体时间 例如:2014-01-14 可以用来记录生日、纪念日、加盟日等。
LocalTime:本地时间,不包含日期。
LocalDateTime:组合了日期和时间,但不包含时差和时区信息。
ZonedDateTime:最完整的日期时间,包含时区和相对UTC或格林威治的时差。
新API还引入了 ZoneOffSet 和 ZoneId 类,使得解决时区问题更为简便。

解析、格式化时间的 DateTimeFormatter 类也进行了全部重新设计。

例如,我们使用 LocalDate 代替 Date,使用 DateTimeFormatter 代替 SimpleDateFormat,如下所示:

 String DateNow = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
        System.out.println(DateNow);

这样就避免了 SimpleDateFormat 的线程不安全问题啦。

  • 6
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
SimpleDateFormat类是Java中用于格式化日期的类,它不是线程安全的。这意味着在多线程环境下同时使用一个SimpleDateFormat实例可能会导致错误的结果或者抛出异常。 要保证SimpleDateFormat线程安全,可以采用以下两种方式之一: 1. 使用ThreadLocal:可以为每个线程创建一个SimpleDateFormat实例,并将其存储在ThreadLocal对象中。这样每个线程都有自己的SimpleDateFormat实例,避免了线程间的竞争条件。 ```java private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); public static String formatDate(Date date) { return dateFormat.get().format(date); } ``` 在上述代码中,每个线程通过`dateFormat.get()`获取自己的SimpleDateFormat实例,并调用其format方法进行日期格式化。 2. 使用局部变量:在每个需要使用SimpleDateFormat方法内部创建一个局部变量,并在使用完毕后及时销毁。这样每个线程都有自己的SimpleDateFormat实例,避免了线程间的竞争条件。 ```java public static String formatDate(Date date) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); return dateFormat.format(date); } ``` 在上述代码中,每个方法都会创建自己的SimpleDateFormat实例,并在使用完毕后销毁,确保线程安全。 这两种方式都能够保证SimpleDateFormat线程安全性,选择哪种方式取决于具体的使用场景和需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值