如何编辑网站源代码,为什么网站建设还要续费,关键词的选取原则,网页设计模板html免费一、前言
日期的转换与格式化在项目中应该是比较常用的了
一个问题#xff1a;项目中的日期转换怎么用的#xff1f;SimpleDateFormat 用过吗#xff1f;能说一下 SimpleDateFormat 线程安全问题吗#xff0c;以及如何解决#xff1f;
回答#xff1a;平时就是在全局定…一、前言
日期的转换与格式化在项目中应该是比较常用的了
一个问题项目中的日期转换怎么用的SimpleDateFormat 用过吗能说一下 SimpleDateFormat 线程安全问题吗以及如何解决
回答平时就是在全局定义一个 static的 SimpleDateFormat然后在业务处理方法controller中直接使用至于线程安全… 这个… 倒是没遇到过线程安全问题。
下面讲解一下。
二、概述
SimpleDateFormat 类主要负责日期的转换与格式化等操作在多线程的环境中使用此类容易造成数据转换及处理的不正确因为 SimpleDateFormat 类并不是线程安全的但在单线程环境下是没有问题的。
SimpleDateFormat 在类注释中也提醒大家不适用于多线程场景
Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized
externally.日期格式不同步。
建议为每个线程创建单独的格式实例。
如果多个线程同时访问一种格式则必须在外部同步该格式。来看看阿里巴巴 java 开发规范是怎么描述 SimpleDateFormat 的 三、模拟线程安全问题
无码无真相接下来我们创建一个线程来模拟 SimpleDateFormat 线程安全问题
创建 MyThread.java 类
public class MyThread extends Thread{private SimpleDateFormat simpleDateFormat;/* 要转换的日期字符串 */private String dateString;public MyThread(SimpleDateFormat simpleDateFormat, String dateString){this.simpleDateFormat simpleDateFormat;this.dateString dateString;}Overridepublic 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();}}
}创建执行类 Test.java 类
public class Test {// 一般我们使用SimpleDateFormat的时候会把它定义为一个静态变量避免频繁创建它的对象实例private static SimpleDateFormat simpleDateFormat new SimpleDateFormat(YYYY-MM-dd);public static void main(String[] args) {String[] dateStringArray new String[] { 2020-09-10, 2020-09-11, 2020-09-12, 2020-09-13, 2020-09-14};MyThread[] myThreads new MyThread[5];// 创建线程for (int i 0; i 5; i) {myThreads[i] new MyThread(simpleDateFormat, dateStringArray[i]);}// 启动线程for (int i 0; i 5; i) {myThreads[i].start();}}
}执行截图如下 从控制台打印的结果来看使用单例的 SimpleDateFormat 类在多线程的环境中处理日期转换极易出现转换异常java.lang.NumberFormatException:multiple points以及转换错误的情况。
四、线程不安全的原因
这个时候就需要看看源码了format() 格式转换方法
// 成员变量 Calendar
protected Calendar calendar;private StringBuffer format(Date date, StringBuffer toAppendTo,FieldDelegate delegate) {// Convert input date to time field listcalendar.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 这个 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 当做一个共享变量来使用。
五、如何解决线程安全
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);}
}所有用到 SimpleDateFormat 的地方全部用 DateUtils 替换然后看一下执行结果 好家伙异常错误终于是没了这种解决处理错误的原理就是创建了多个 SimpleDateFormat 类的实例在需要用到的地方创建一个新的实例就没有线程安全问题不过也加重了创建对象的负担会频繁地创建和销毁对象效率较低。
2、synchronized 锁
synchronized 就不展开介绍了
变更一下 DateUtils.java
public class DateUtils {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);}}
}简单粗暴synchronized 往上一套也可以解决线程安全问题缺点自然就是并发量大的时候会对性能有影响因为使用了 synchronized 加锁后的多线程就相当于串行线程阻塞执行效率低。
3、ThreadLocal最佳MVP
ThreadLocal 是 java 里一种特殊的变量ThreadLocal 提供了线程本地的实例它与普通变量的区别在于每个使用该线程变量的线程都会初始化一个完全独立的实例副本。
继续改造 DateUtils.java
public class DateUtils {private static ThreadLocalDateFormat threadLocal new ThreadLocalDateFormat(){Overrideprotected 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);}
}ThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象那么就不会存在竞争问题。
如果项目中还在使用 SimpleDateFormat 的话推荐这种写法但这样就结束了吗
显然不是…
六、项目中推荐的写法
上边提到的阿里巴巴 java 开发手册给出了说明如果是 JDK8 的应用可以使用 Instant 代替 DateLocalDateTime 代替 CalendarDateTimeFormatter 代替 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 的线程不安全问题啦。
此时的 DateUtils.java
public class DateUtils {public static final DateTimeFormatter DATE_TIME_FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd);public static LocalDate parse(String dateString){return LocalDate.parse(dateString, DATE_TIME_FORMATTER);}public static String format(LocalDate target) {return target.format(DATE_TIME_FORMATTER);}
}七、最后总结
SimpleDateFormart 线程不安全问题SimpleDateFormart 继承自 DateFormart在 DataFormat 类内部有一个 Calendar 对象引用SimpleDateFormat 转换日期都是靠这个 Calendar 对象来操作的比如 parse(String)format(date) 等类似的方法Calendar 在用的时候是直接使用的而且是改变了 Calendar 的值这样情况在多线程下就会出现线程安全问题如果 SimpleDateFormart 是静态的话那么多个 thread 之间就会共享这个 SimpleDateFormart同时也会共享这个 Calendar 引用那么就出现数据赋值覆盖情况也就是线程安全问题。(现在项目中用到日期转换都是使用的 java 8 中的 LocalDate或者 LocalDateTime本质是这些类是不可变类不可变一定程度上保证了线程安全)。
解决方式在多线程下可以使用 ThreadLocal 修饰 SimpleDateFormartThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象那么就不会存在竞争问题。
项目中推荐的写法java 8 中引入新的日期类 API这些类是不可变的且线程安全的。