Date类型和SimpleDateFormat的弊端
对于一个Java开发来说,DateUtil这个工具类,大家一定不陌生。每个项目中,都会封装这样一个工具类,在其中提供了当前时间获取,当前日期获取,以及时间或日期形式和字符串形式的各种格式的相互转换,用来解决日期形式繁杂的转换。然而就算是封装好了,每次用到日期形式转换的时候,都要点进工具类里去查看方法的实现,才能知道自己想要的是哪个。虽然麻烦,但是在单线程的应用中倒也没多少问题。
然而在多线程的应用中,日期的格式化则有可能出现问题。看下面的一个实例:
private static void simpleDateTime(){
// 创建calendar类
Calendar startDate = Calendar.getInstance();
// 设置开始时间
startDate.set(1990,01,01,1,0,0);
List<Date> dateList = new ArrayList<>();
// 以一个月为间隔创建200个时间
for(int i = 0; i<200;i++){
startDate.add(Calendar.MONTH,1);
dateList.add(startDate.getTime());
}
// 创建线程公用的SimpleDateFormat
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// 创建线程池,固定核心线程为2
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 创建线程(此处使用了lambda表达式)
dateList.forEach(date ->{
// 将线程加入线程池中
executorService.submit(new Thread(()->{
//转换日期格式
String testDat = sdf.format(date);
System.out.println("simpleDateTime:"+Thread.currentThread().getName()+":"+date +":"+testDat);
}));
});
// 关闭线程池
executorService.shutdown();
}
输出结果如下(只截取了能说明问题的部分结果):
这两处的格式转换前和转换后不一致。出现这个问题的原因就是SimpleDateFormat的线程不安全性导致的。关于这个问题的说明,网上有很多可以自行查阅。
综上,Date以及Calendar类型操作复杂,用于处理时间格式的SimpleDateFormat又不能保证线程安全,这两点一直被Java开发者诟病。对于SimpleDateFormat的非线程安全问题,网上也有很多解决的方案,大致列举如下:
- 在每个线程实例中都新建一个SimpleDateFormat实例。缺点:每次创建线程实例都要新建format对象,消耗内存,增加GC负担。
- 给SimpleDateFormat对象加锁,使用Lock或者synchronized修饰。缺点:个线程线性执行,性能差。
- 使用ThreadLocal为每个线程创建一个SimpleDateFormat对象副本。优点是有线程隔离性,各自的副本对象不会影响其他副本。
以上解决方案中,数方案三最优,但即便如此,也增加了代码的复杂度。
jdk8中的全新时间类型
在jdk8中,加入了全新的时间,日期类型。然而很多Java开发者都不知道或者说不了解这些新的类型。
看看官方文档如何描述LocalDateTime:
Implementation Requirements:
This class is immutable and thread-safe.
详细说明
意思就是说,此类是不可变的并且是线程安全的。并且支持的日期范围,我认为已经扩大到了人类文明用不完(开玩笑)。
当然最重要的有两点,这些时间类型
- 线程安全。
- 使用非常方便,简单,无需封装。
是时候和DateUtil工具类说再见了。
为什么是线程安全的?
那么,LocalDateTime的不可变指的是什么呢?又是怎样保证线程安全性的?
上源码:
public final class LocalDateTime
implements Temporal, TemporalAdjuster, ChronoLocalDateTime<LocalDate>, Serializable {
/**
* The minimum supported {@code LocalDateTime}, '-999999999-01-01T00:00:00'.
* This is the local date-time of midnight at the start of the minimum date.
* This combines {@link LocalDate#MIN} and {@link LocalTime#MIN}.
* This could be used by an application as a "far past" date-time.
*/
public static final LocalDateTime MIN = LocalDateTime.of(LocalDate.MIN, LocalTime.MIN);
/**
* The maximum supported {@code LocalDateTime}, '+999999999-12-31T23:59:59.999999999'.
* This is the local date-time just before midnight at the end of the maximum date.
* This combines {@link LocalDate#MAX} and {@link LocalTime#MAX}.
* This could be used by an application as a "far future" date-time.
*/
public static final LocalDateTime MAX = LocalDateTime.of(LocalDate.MAX, LocalTime.MAX);
/**
* Serialization version.
*/
private static final long serialVersionUID = 6207766400415563566L;
/**
* The date part.
*/
private final LocalDate date;
/**
* The time part.
*/
private final LocalTime time;
LocalDate类是final类型的,也就是说,LocalDate是不可变的,一旦实例化,值就固定了。而java8之前的Date类不是final的。这就是此类不可变的含义所在。在LocalDateTime类的format方法中,也使用了格式化工具类DateTimeFormatter,不同于SimpleDateFormat的是,这个DateTimeFormatter也是不可变且线程安全的。
final关键字的内存语义
1,写final域的重排序规则:JMM禁止编译器把final域的写重排序到构造函数初始化之外(之后)。编译器会在final域的写之后,构造函数return之前,插入一个StoreStore内存屏障。
2,读final域的重排序规则:在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止重排序这2个操作。编译器会在读final域操作的前面插入一个LoadLoad屏障。
解读:读final域的重排序规则可以确保,在读一个对象的final域之前,一定会先读这个final域的对象引用。也就是说,可以确保final修饰的对象this.fieldfield是true。但是普通的非final修饰的对象,不能确保this.fieldfield是true。
以上内容引用自Java8提供的LocalDate和DateTimeFormat是如何保证线程安全的?
还是可以通过代码来验证是不是这样。代码如下:
private static void localDateTime(){
LocalDateTime startTime = LocalDateTime.of(1990,01,01,1,0,0);
List<LocalDateTime> dateList = new ArrayList<>();
for(int i = 0; i<200;i++){
dateList.add(startTime.plusMonths(i));
}
ExecutorService executorService = Executors.newFixedThreadPool(2);
dateList.forEach(date ->{
executorService.submit(new Thread(()->{
String testLdt = date.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println("localDateTime:"+Thread.currentThread().getName()+":"+date +":"+testLdt);
}));
});
executorService.shutdown();
}
输出结果是,200个转换的结果和原日期一致。这里就不截图了。
使用方法
- 按照自由格式获取当前时间
public static String getTodayByFormat(String timeFormat){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(timeFormat));
}
-
获取当前年/月/日
-
获取前一天
public static String getYesterdayByFormat(String timeFormat){
return LocalDateTime.now().minusDays(1).format(DateTimeFormatter.ofPattern(timeFormat));
}
同理,获取后一天用plusDays方法。其他plus和minus方法如下:
值得注意的是,所有方法的返回值都是LocalDateTime,也就意味着可以一直链式点下去。例如想要一年三个月12天后的数据,也能轻松得到。
- 获取指定时间的日期类型。简单
LocalDateTime startTime = LocalDateTime.of(1990,01,01,1,0,0);
和calendar一样。
// 根据字符串取:
LocalDate endOfFeb = LocalDate.parse("2014-02-28"); // 严格按照ISO yyyy-MM-dd验证,02写成2都不行,当然也有一个重载方法允许自己定义格式
LocalDate.parse("2014-02-29"); // 无效日期无法通过:DateTimeParseException: Invalid date
- 其他特殊要求的日期获取
// 取本月第1天:
LocalDate firstDayOfThisMonth = today.with(TemporalAdjusters.firstDayOfMonth()); // 2019-10-01
// 取本月第2天:
LocalDate secondDayOfThisMonth = today.withDayOfMonth(2); // 2019-10-02
// 取本月最后一天,再也不用计算是28,29,30还是31:
LocalDate lastDayOfThisMonth = today.with(TemporalAdjusters.lastDayOfMonth()); // 2019-10-31
// 取下个月第一天:
LocalDate firstDayOf2019 = lastDayOfThisMonth.plusDays(1); // 变成了2019-11-01
// 取2019年1月第一个周一,这个计算用Calendar要死掉很多脑细胞:
LocalDate firstMondayOf2019 = LocalDate.parse("2019-01-01").with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)); // 2015-01-05
注意:TemporalAdjusters是日期形式调整工具类。详情参见TemporalAccessor官方文档