概述
在 Java8 之前, 我们一般都是使用 SimpleDateFormat 来解析和格式化日期时间, 但它是线程不安全的.
- @Test
- public void test() {
- SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
- ExecutorService executorService = Executors.newFixedThreadPool(5);
- for (int i = 0; i <10; i++) {
- executorService.execute(() -> {
- try {
- Date date = sdf.parse("20191103091515");
- System.out.println(date.toString());
- } catch (ParseException e) {
- e.printStackTrace();
- }
- });
- }
- executorService.shutdown();
- }
多次运行上面这段程序, 会报不同的异常, 下面是其中的一种
- Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-3" java.lang.NumberFormatException: empty String
- at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
- at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
- at java.lang.Double.parseDouble(Double.java:538)
- at java.text.DigitList.getDouble(DigitList.java:169)
- at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
- at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
- at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
- at java.text.DateFormat.parse(DateFormat.java:364)
- at com.java8.action.Demo.lambda$test$0(Demo.java:25)
- at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
- at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
- at java.lang.Thread.run(Thread.java:748)
原因也很简单, 查看一下源码, 发现 SimpleDateFormat 类继承了父类 DateFormat 的成员变量 protected Calendar calendar; , 而 Calendar 类没有被 final 修饰, 是可以被修改的.
回到上面这个问题, 看一下 SimpleDateFormat 的解析日期时间的 API
进入 establish() 方法里面看一下
到此, 已经基本明了, 因为每次 SimpleDateFormat 解析日期时间都会清空一下它的成员变量 calendar 的值, 所以当多个线程并发访问同一个 SimpleDateformat 时, 就会有线程不安全问题.
解决方式也很简单, 你可以使用 ThreadLocal 类存放 SimpleDateFormat 对象, 让每个线程拥有自己的 SimpleDateFormat 对象.
- /**Map 键对应不同的解析规则字符串, 比如 yyyyMMdd*/
- private static Map<String, ThreadLocal<SimpleDateFormat>> tl = new HashMap<>();
回到我们今天的主题, 在 Java8 中引入了新的日期和时间 API, 这也是下面要介绍的内容.
新的日期时间类都被 final 修饰, 不存在想上面介绍的老版本 API 的线程不安全问题.
LocalDate,LocalTime 和 LocalDateTime
LocalDate 和 LocalTime, LocalDateTime 提供了许多静态工厂方法来创建它们的实例对象, 并且这三者之间可以很方便的互相进行类型转换.
- @Test
- public void test() {
- // 静态方法创建对象
- LocalDate ld = LocalDate.of(2019, 10, 3);
- System.out.println(ld.getYear() + "\t" + ld.getMonth() + "\t" + ld.getDayOfMonth() + "\t" + ld.getDayOfWeek() + "\t" + ld.lengthOfMonth() + "\t" + ld.isLeapYear());//result: 2019 OCTOBER 3 THURSDAY 31 false
- LocalDate now = LocalDate.now();
- System.out.println(now.get(ChronoField.YEAR) + "\t" + now.get(ChronoField.MONTH_OF_YEAR) + "\t" + now.get(ChronoField.DAY_OF_MONTH));//result: 2019 11 3
- LocalTime lt = LocalTime.of(20, 44, 12);
- System.out.println(lt.getHour() + "\t" + lt.getMinute() + "\t" + lt.getSecond());//result: 20 44 12
- // 解析字符串
- LocalDate ld2 = LocalDate.parse("2019-10-05");// 默认格式: yyyy-MM-dd
- System.out.println(ld2.toString());//result: 2019-10-05
- LocalTime lt2 = LocalTime.parse("20:42:12.828");// 默认格式: HH:mm:ss.SSS
- System.out.println(lt2.toString());//result: 20:42:12.828
- // 互相进行类型转换
- LocalDateTime ldt = LocalDateTime.of(2019, 10, 5, 21, 12, 10, 888).atZone(ZoneId.of("Asia/Shanghai")).toLocalDateTime();
- LocalDateTime ldt2 = LocalDateTime.of(ld2, lt2);// 2019-10-05T20:42:12.828
- LocalDateTime ldt3 = ld2.atTime(10, 10, 10);// 2019-10-05T10:10:10
- LocalDateTime ldt4 = ld2.atTime(lt2);
- LocalDateTime ldt5 = lt2.atDate(ld2);
- LocalDateTime ldt6 = LocalDateTime.parse("2019/10/05 20:20:20.888", DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS"));
- LocalDate ld6 = ldt6.toLocalDate();
- LocalTime lt6 = ldt6.toLocalTime();
- }
- Instant
Instant 对时间的建模方式是以 UTC 时区的 1970 年 1 月 1 日午夜时分开始所经历的秒数进行计算, 它不包含时区信息. Instant 类是为了方便计算机处理日期和时间而设计的.
Instant.now().toEpochMilli() 可以获取当前时间的时间戳, 另外, Instant 提供了类似 ofEpochMilli() 的方法根据某个时间戳获取 Instant 实例, isBefore()和 isAfter() 则用来比较两个 Instant 的大小
- @Test
- public void test2() {
- long milli = Instant.now().toEpochMilli();// 获取当前时间戳
- Instant instant = Instant.ofEpochMilli(1572749169937L);// 根据某个时间戳获取 Instant 实例
- Instant instant2 = instant.minusSeconds(1000L);
- System.out.println(instant.isAfter(instant2));//true
- }
Duration 和 Period
Duration 对象用秒和纳秒来衡量时间的长短, 如果想要对多个时间对象进行日期运算, 可以用 Period 类
- @Test
- public void test3() {
- Duration d1 = Duration.between(LocalDateTime.of(2019, 10, 7, 15, 55, 55, 888), LocalDateTime.now());
- Duration d2 = Duration.between(LocalTime.of(17, 55, 10), LocalTime.now());
- Duration d3 = Duration.between(Instant.ofEpochMilli(1570544602000L), Instant.now());
- System.out.println(d3.toHours());// 612
- //Duration 对象用秒和纳秒来衡量时间的长短, 所以入参不能使用 LocalDate 类型, 否则抛 UnsupportedTemporalTypeException: Unsupported unit: Seconds
- //Duration.between(LocalDate.of(2019, 10, 7), LocalDate.now());
- // 如果想要对多个时间对象进行日期运算, 可以用 Period
- Period p1 = Period.between(LocalDate.of(2018, 8, 30), LocalDate.now());
- System.out.println(p1.getYears() + "\t" + p1.getMonths() + "\t" + p1.getDays());// 1 2 4
- // 工厂方法介绍
- Duration threeMinutes = Duration.ofMinutes(3);
- threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
- Period tenDays = Period.ofDays(10);
- Period threeWeeks = Period.ofWeeks(3);
- Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
- }
Temporal 接口
LocalDate,LocalTime,LocalDateTime,Instant 类都实现了 Temporal 接口, 有很多通用的处理日期和时间的方法, 比如 plus(), minus(), with()
- @Test
- public void test4() {
- LocalDate ld = LocalDate.of(2019, 10, 7);
- // 修改时间对象的某个属性值, 返回一个新的对象
- LocalDate ld2 = ld.withDayOfYear(365);//2019-12-31
- LocalDate ld3 = ld.withDayOfMonth(18);//2019-10-18
- LocalDate ld4 = ld.with(ChronoField.MONTH_OF_YEAR, 8);//2019-08-07
- // 对时间对象进行加减运算
- LocalDate ld5 = ld.plusWeeks(2L);//2019-10-21
- LocalDate ld6 = ld.minusYears(9L);//2010-10-07
- LocalDate ld7 = ld.plus(Period.ofMonths(2));//2019-12-07
- LocalDate ld8 = ld.plus(2L, ChronoUnit.MONTHS);//2019-12-07
- LocalTime lt = LocalTime.parse("10:10:10.888");
- LocalTime lt1 = lt.plus(Duration.ofHours(2L));//12:10:10.888
- LocalTime lt2 = lt.plus(120L, ChronoUnit.MINUTES);//12:10:10.888
- }
TemporalAdjuster 接口
TemporalAdjuster 类提供了更多对日期定制化操作的功能, 诸如将日期调整到下个工作日, 本月的最后的一天, 今年的第一天等等.
- @Test
- public void test5() {
- LocalDate ld = LocalDate.of(2019, 10, 7);
- LocalDate ld1 = ld.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));//2019-10-11
- LocalDate ld2 = ld.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));//2019-10-07
- LocalDate ld3 = ld.with(TemporalAdjusters.firstDayOfNextMonth());//2019-11-01
- // 自定义 TemporalAdjuster, 来计算下一个工作日所在的日期
- LocalDate ld4 = LocalDate.of(2019, 10, 11).with(temporal -> {
- DayOfWeek now = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
- long dayToAdd = now.equals(DayOfWeek.FRIDAY) ? 3L : now.equals(DayOfWeek.SATURDAY) ? 2L : 1L;
- return temporal.plus(dayToAdd, ChronoUnit.DAYS);
- });//2019-10-14
- // 对于经常复用的相同操作, 可以将逻辑封装一个类中
- TemporalAdjuster temporalAdjuster = TemporalAdjusters.ofDateAdjuster(temporal -> {
- DayOfWeek now = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
- long dayToAdd = now.equals(DayOfWeek.FRIDAY) ? 3L : now.equals(DayOfWeek.SATURDAY) ? 2L : 1L;
- return temporal.plus(dayToAdd, ChronoUnit.DAYS);
- });
- }
- DateTimeFormatter
DateTimeFormatter 用于进行可定制的日期时间格式化, 功能相当于以前的 SimpleDateFormat 类
- @Test
- public void test6() {
- // 日期转字符串
- LocalDate ld = LocalDate.of(2019, 10, 7);
- String s1 = ld.format(DateTimeFormatter.BASIC_ISO_DATE);//20191007
- String s2 = ld.format(DateTimeFormatter.ISO_LOCAL_DATE);//2019-10-07
- // 字符串转日期
- LocalDateTime ld1 = LocalDateTime.parse("2019-10-07 22:22:22.555", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
- }
ZoneId 时区
ZoneId 是老版本的 TimeZone 的替代品, ZonedDateTime 代表了相对于指定时区的时间点
- @Test
- public void test7() {
- //LocalDate,LocalDateTime,Instant 转 ZonedDateTime
- ZonedDateTime zdt1 = LocalDate.of(2019, 10, 7).atStartOfDay(ZoneId.systemDefault());
- ZonedDateTime zdt2 = LocalDateTime.of(2019, 10, 7, 15, 55, 55, 888).atZone(ZoneId.of("Asia/Shanghai"));
- ZonedDateTime zdt3 = Instant.now().atZone(ZoneId.of("Asia/Yerevan"));
- //Instant 转 LocalDateTime
- LocalDateTime ldt1 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
- // 下面的两个栗子介绍了 ZoneOffset, 他是利用和 UTC / 格林尼治时间的固定偏差计算时区, 但不推荐使用, 因为 ZoneOffset 并未考虑任何夏令时的影响
- LocalDateTime ldt2 = LocalDateTime.ofInstant(Instant.now(), ZoneOffset.of("+8"));
- //LocalDateTime 转 Instant
- Instant instant = LocalDateTime.of(2019, 10, 7, 15, 55, 55).toInstant(ZoneOffset.of("+4"));
- }
参考资料
Java8 实战 https://book.douban.com/subject/26772632//
SimpleDateFormat 线程不安全及解决办法
Java 进阶 (七) 正确理解 Thread Local 的原理与适用场景 http://www.jasongj.com/java/threadlocal/
来源: https://www.cnblogs.com/qingshanli/p/11784068.html