小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动
本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
❤️作者简介:Java领域优质创作者🏆,CSDN博客专家认证🏆,华为云享专家认证🏆
❤️技术活,该赏
❤️点赞 👍 收藏 ⭐再看,养成习惯
介绍
本篇文章主要介绍java源码中提供了哪些日期和时间的类
日期和时间的两套API
java提供了两套处理日期和时间的API
1、旧的API,放在java.util 这个包下的:比较常用的有Date和Calendar等
2、新的API是java 8新引入的,放在java.time 这个包下的:LocalDateTime,ZonedDateTime,DateTimeFormatter和Instant等
为什么会有两套日期时间API,这个是有历史原因的,旧的API是jdk刚开始就提供的,随着版本的升级,逐渐发现原先的api不满足需要,暴露了一些问题,所以在java 8 这个版本中,重新引入新API。
这两套API都要了解,为什么呢?
因为java 8 发布时间是2014年,很多之前的系统还是沿用旧的API,所以这两套API都要了解,同时还要掌握两套API相互转化的技术。
一:Date
支持版本及以上
JDK1.0
介绍
Date类说明
Date类负责时间的表示,在计算机中,时间的表示是一个较大的概念,现有的系统基本都是利用从1970.1.1 00:00:00 到当前时间的毫秒数进行计时,这个时间称为epoch(时间戳)
1 | java复制代码package java.util; |
java.util.Date是java提供表示日期和时间的类,类里有个long 类型的变量fastTime,它是用来存储以毫秒表示的时间戳。
date常用的用法
1 | java复制代码import java.util.Date; |
自定义时间格式-SimpleDateFormat
date的toString方法转成字符串,不是我们想要的时间格式,如果要自定义时间格式,就要使用SimpleDateFormat
1 | java复制代码 //获取当前时间 |
SimpleDateFormat也可以方便的将字符串转成Date
1 | java复制代码//获取当前时间 |
日期和时间格式化参数说明
1 | ini复制代码yyyy:年 |
SimpleDateFormat线程不安全原因及解决方案
SimpleDateFormat线程为什么是线程不安全的呢?
来看看SimpleDateFormat的源码,先看format方法:
1 | java复制代码// Called from Format after creating a FieldDelegate |
问题就出在成员变量calendar,如果在使用SimpleDateFormat时,用static定义,那SimpleDateFormat变成了共享变量。那SimpleDateFormat中的calendar就可以被多个线程访问到。
SimpleDateFormat的parse方法也是线程不安全的:
1 | java复制代码 public Date parse(String text, ParsePosition pos) |
由源码可知,最后是调用**parsedDate = calb.establish(calendar).getTime();**获取返回值。方法的参数是calendar,calendar可以被多个线程访问到,存在线程不安全问题。
我们再来看看**calb.establish(calendar)**的源码
calb.establish(calendar)方法先后调用了cal.clear()和cal.set(),先清理值,再设值。但是这两个操作并不是原子性的,也没有线程安全机制来保证,导致多线程并发时,可能会引起cal的值出现问题了。
验证SimpleDateFormat线程不安全
1 | java复制代码public class SimpleDateFormatDemoTest { |
出现了两次false,说明线程是不安全的。而且还抛异常,这个就严重了。
解决方案
这个是阿里巴巴 java开发手册中的规定:
1、不要定义为static变量,使用局部变量
2、加锁:synchronized锁和Lock锁
3、使用ThreadLocal方式
4、使用DateTimeFormatter代替SimpleDateFormat(DateTimeFormatter是线程安全的,java 8+支持)
5、使用FastDateFormat 替换SimpleDateFormat(FastDateFormat 是线程安全的,Apache Commons Lang包支持,不受限于java版本)
解决方案1:不要定义为static变量,使用局部变量
就是要使用SimpleDateFormat对象进行format或parse时,再定义为局部变量。就能保证线程安全。
1 | java复制代码public class SimpleDateFormatDemoTest1 { |
由图可知,已经保证了线程安全,但这种方案不建议在高并发场景下使用,因为会创建大量的SimpleDateFormat对象,影响性能。
解决方案2:加锁:synchronized锁和Lock锁
加synchronized锁:
SimpleDateFormat对象还是定义为全局变量,然后需要调用SimpleDateFormat进行格式化时间时,再用synchronized保证线程安全。
1 | java复制代码public class SimpleDateFormatDemoTest2 { |
如图所示,线程是安全的。定义了全局变量SimpleDateFormat,减少了创建大量SimpleDateFormat对象的损耗。但是使用synchronized锁,
同一时刻只有一个线程能执行锁住的代码块,在高并发的情况下会影响性能。但这种方案不建议在高并发场景下使用
加Lock锁:
加Lock锁和synchronized锁原理是一样的,都是使用锁机制保证线程的安全。
1 | java复制代码public class SimpleDateFormatDemoTest3 { |
由结果可知,加Lock锁也能保证线程安全。要注意的是,最后一定要释放锁,代码里在finally里增加了lock.unlock();,保证释放锁。
在高并发的情况下会影响性能。这种方案不建议在高并发场景下使用
解决方案3:使用ThreadLocal方式
使用ThreadLocal保证每一个线程有SimpleDateFormat对象副本。这样就能保证线程的安全。
1 | java复制代码public class SimpleDateFormatDemoTest4 { |
使用ThreadLocal能保证线程安全,且效率也是挺高的。适合高并发场景使用。
解决方案4:使用DateTimeFormatter代替SimpleDateFormat使用DateTimeFormatter代替SimpleDateFormat(DateTimeFormatter是线程安全的,java 8+支持)DateTimeFormatter介绍
1 | java复制代码public class DateTimeFormatterDemoTest5 { |
使用DateTimeFormatter能保证线程安全,且效率也是挺高的。适合高并发场景使用。
解决方案5:使用FastDateFormat 替换SimpleDateFormat
使用FastDateFormat 替换SimpleDateFormat(FastDateFormat 是线程安全的,Apache Commons Lang包支持,不受限于java版本)
1 | java复制代码public class FastDateFormatDemo6 { |
使用FastDateFormat能保证线程安全,且效率也是挺高的。适合高并发场景使用。
FastDateFormat源码分析
1 | 复制代码 Apache Commons Lang 3.5 |
1 | java复制代码//FastDateFormat |
源码中 Calender 是在 format 方法里创建的,肯定不会出现 setTime 的线程安全问题。这样线程安全疑惑解决了。那还有性能问题要考虑?
我们来看下FastDateFormat是怎么获取的
1 | java复制代码FastDateFormat.getInstance(); |
看下对应的源码
1 | java复制代码/** |
这里有用到一个CACHE,看来用了缓存,往下看
1 | java复制代码private static final FormatCache<FastDateFormat> CACHE = new FormatCache<FastDateFormat>(){ |
在getInstance 方法中加了ConcurrentMap 做缓存,提高了性能。且我们知道ConcurrentMap 也是线程安全的。
实践
1 | java复制代码/** |
1 | java复制代码//FastDateFormat |
如图可证,是使用了ConcurrentMap 做缓存。且key值是格式,时区和locale(语境)三者都相同为相同的key。
问题
1、tostring()输出时,总以系统的默认时区格式输出,不友好。
2、时区不能转换
3、日期和时间的计算不简便,例如计算加减,比较两个日期差几天等。
4、格式化日期和时间的SimpleDateFormat对象是线程不安全的
5、Date对象本身也是线程不安全的
1 | java复制代码public class Date |
二:Calendar
支持版本及以上
JDK1.1
介绍
Calendar类说明
Calendar类提供了获取或设置各种日历字段的各种方法,比Date类多了一个可以计算日期和时间的功能。
Calendar常用的用法
1 | java复制代码 // 获取当前时间: |
如上图所示,月份计算时,要+1;返回的星期是从周日开始计算,周日为1,1~7表示星期;
Calendar的跨年问题和解决方案
问题
背景:在使用Calendar 的api getWeekYear()读取年份,在跨年那周的时候,程序获取的年份可能不是我们想要的,例如在2019年30号时,要返回2019,结果是返回2020,是不是有毒
1 | ini复制代码// 获取当前时间: |
分析原因
老规矩,从源码入手
1 | java复制代码Calendar类 |
这个源码有点奇怪,getWeekYear()方法是java 7引入的。它的实现怎么是抛出异常,但是执行时,又有结果返回。
断点跟进,通过Calendar.getInstance()获取的Calendar实例是GregorianCalendar
1 | java复制代码GregorianCalendar |
方法内获取的年份刚开始是正常的
在JDK中会把前一年末尾的几天判定为下一年的第一周,因此上面程序的结果是1
解决方案
使用Calendar类 get(Calendar.YEAR)获取年份
问题
1、读取月份时,要+1
2、返回的星期是从周日开始计算,周日为1,1~7表示星期
3、Calendar的跨年问题,获取年份要用c.get(Calendar.YEAR),不要用c.getWeekYear();
4、获取指定时间是一年中的第几周时,调用cl.get(Calendar.WEEK_OF_YEAR),要注意跨年问题,跨年的那一周,获取的值为1。离跨年最近的那周为52。
三:LocalDateTime
支持版本及以上
jdk8
介绍
LocalDateTime类说明
表示当前日期时间,相当于:yyyy-MM-ddTHH:mm:ss
LocalDateTime常用的用法
获取当前日期和时间
1 | java复制代码 LocalDate d = LocalDate.now(); // 当前日期 |
由运行结果可行,本地日期时间通过now()获取到的总是以当前默认时区返回的
获取指定日期和时间
1 | java复制代码 LocalDate d2 = LocalDate.of(2021, 07, 14); // 2021-07-14, 注意07=07月 |
日期时间的加减法及修改
1 | java复制代码 LocalDateTime currentTime = LocalDateTime.now(); // 当前日期和时间 |
LocalDateTime和Date相互转化
Date转LocalDateTime
1 | java复制代码 System.out.println("------------------方法一:分步写-----------------------"); |
LocalDateTime转Date
1 | java复制代码 System.out.println("------------------方法一:分步写-----------------------"); |
线程安全
网上大家都在说JAVA 8提供的LocalDateTime是线程安全的,但是它是如何实现的呢
今天让我们来挖一挖
1 | java复制代码public final class LocalDateTime |
由上面的源码可知,LocalDateTime是不可变类。我们都知道一个Java并发编程规则:不可变对象永远是线程安全的。
对比下Date的源码 ,Date是可变类,所以是线程不安全的。
1 | java复制代码public class Date |
四:ZonedDateTime
支持版本及以上
jdk8
介绍
ZonedDateTime类说明
表示一个带时区的日期和时间,ZonedDateTime可以理解为LocalDateTime+ZoneId
从源码可以看出来,ZonedDateTime类中定义了LocalDateTime和ZoneId两个变量。
且ZonedDateTime类也是不可变类且是线程安全的。
1 | java复制代码public final class ZonedDateTime |
ZonedDateTime常用的用法
获取当前时间+带时区+时区转换
1 | java复制代码 // 默认时区获取当前时间 |
LocalDateTime+ZoneId变ZonedDateTime
1 | java复制代码 LocalDateTime localDateTime = LocalDateTime.now(); |
上面的例子说明了,LocalDateTime是可以转成ZonedDateTime的。
五:DateTimeFormatter
支持版本及以上
jdk8
介绍
DateTimeFormatter类说明
DateTimeFormatter的作用是进行格式化显示,且DateTimeFormatter是不可变类且是线程安全的。
1 | java复制代码public final class DateTimeFormatter { |
java复制代码 ZonedDateTime zonedDateTime = ZonedDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(“yyyy-MM-dd’T’HH:mm ZZZZ”);
System.out.println(formatter.format(zonedDateTime));
DateTimeFormatter usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
System.out.println(usFormatter.format(zonedDateTime));
DateTimeFormatter chinaFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
System.out.println(chinaFormatter.format(zonedDateTime));
1 |
|
java复制代码 DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(“yyyy-MM-dd HH:mm:ss.SSS”);
LocalDateTime dt1 = LocalDateTime.parse(“2021-7-20 23:46:43.946”, DATE_TIME_FORMATTER);
System.out.println(dt1);
1 |
|
java复制代码java.time.format.DateTimeParseException: Text ‘2021-7-20 23:46:43.946’ could not be parsed at index 5
1 |
|
java复制代码 DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(“yyyy-MM-dd HH:mm:ss.SSS”);
LocalDateTime dt1 = LocalDateTime.parse(“2021-07-20 23:46:43.946”, DATE_TIME_FORMATTER);
System.out.println(dt1);
1 |
|
java复制代码 LocalDate date = LocalDate.of(2020,12,31);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(“YYYYMM”);
// 结果是 202112
System.out.println( formatter.format(date));
1 |
|
java复制代码Java’s DateTimeFormatter pattern “YYYY” gives you the week-based-year, (by default, ISO-8601 standard) the year of the Thursday of that week.
1 |
|
java复制代码 private static void tryit(int Y, int M, int D, String pat) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pat);
LocalDate dat = LocalDate.of(Y,M,D);
String str = fmt.format(dat);
System.out.printf(“Y=%04d M=%02d D=%02d “ +
“formatted with “ +
“"%s" -> %s\n”,Y,M,D,pat,str);
}
public static void main(String[] args){
tryit(2020,01,20,”MM/DD/YYYY”);
tryit(2020,01,21,”DD/MM/YYYY”);
tryit(2020,01,22,”YYYY-MM-DD”);
tryit(2020,03,17,”MM/DD/YYYY”);
tryit(2020,03,18,”DD/MM/YYYY”);
tryit(2020,03,19,”YYYY-MM-DD”);
}
1 |
ini复制代码Y=2020 M=01 D=20 formatted with “MM/DD/YYYY” -> 01/20/2020
Y=2020 M=01 D=21 formatted with “DD/MM/YYYY” -> 21/01/2020
Y=2020 M=01 D=22 formatted with “YYYY-MM-DD” -> 2020-01-22
Y=2020 M=03 D=17 formatted with “MM/DD/YYYY” -> 03/77/2020
Y=2020 M=03 D=18 formatted with “DD/MM/YYYY” -> 78/03/2020
Y=2020 M=03 D=19 formatted with “YYYY-MM-DD” -> 2020-03-79
1 |
|
java复制代码public final class Instant
implements Temporal, TemporalAdjuster, Comparable
…
}
1 |
|
java复制代码 …
private final long seconds;
private final int nanos;
...
1 |
|
java复制代码 Instant now = Instant.now();
System.out.println(“now:”+now);
System.out.println(now.getEpochSecond()); // 秒
System.out.println(now.toEpochMilli()); // 毫秒
1 |
|
java复制代码 Instant ins = Instant.now();
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
System.out.println(zdt);
1 |
|
java复制代码//1626796436 为秒级时间戳
Instant ins = Instant.ofEpochSecond(1626796436);
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
System.out.println(“秒级时间戳转化:”+zdt);
//1626796436111l 为秒级时间戳
Instant ins1 = Instant.ofEpochMilli(1626796436111l);
ZonedDateTime zdt1 = ins1.atZone(ZoneId.systemDefault());
System.out.println(“毫秒级时间戳转化:”+zdt1);
1 |
|
java复制代码public static Instant now() {
return Clock.systemUTC().instant();
}
1 |
|
java复制代码Instant now = Instant.now().plusMillis(TimeUnit.HOURS.toMillis(8));
System.out.println(“now:”+now);

推荐相关文章
======
hutool日期时间系列文章
--------------
[1DateUtil(时间工具类)-当前时间和当前时间戳](https://blog.csdn.net/shi_hong_fei_hei/article/details/117254005)
[2DateUtil(时间工具类)-常用的时间类型Date,DateTime,Calendar和TemporalAccessor(LocalDateTime)转换](https://blog.csdn.net/shi_hong_fei_hei/article/details/117304856)
[3DateUtil(时间工具类)-获取日期的各种内容](https://blog.csdn.net/shi_hong_fei_hei/article/details/117305070)
[4DateUtil(时间工具类)-格式化时间](https://blog.csdn.net/shi_hong_fei_hei/article/details/117305616)
[5DateUtil(时间工具类)-解析被格式化的时间](https://blog.csdn.net/shi_hong_fei_hei/article/details/117305724)
[6DateUtil(时间工具类)-时间偏移量获取](https://blog.csdn.net/shi_hong_fei_hei/article/details/117430937)
[7DateUtil(时间工具类)-日期计算](https://blog.csdn.net/shi_hong_fei_hei/article/details/117431081)
[8ChineseDate(农历日期工具类)](https://blog.csdn.net/shi_hong_fei_hei/article/details/117431150)
[9LocalDateTimeUtil(JDK8+中的{@link LocalDateTime} 工具类封装)](https://blog.csdn.net/shi_hong_fei_hei/article/details/117431359)
[10TemporalAccessorUtil{@link TemporalAccessor} 工具类封装](https://blog.csdn.net/shi_hong_fei_hei/article/details/117431538)
其他
--
[要探索JDK的核心底层源码,那必须掌握native用法](https://blog.csdn.net/shi_hong_fei_hei/article/details/118658071)
**本文转载自:** [掘金](https://juejin.cn/post/7023961450859233293)
*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*