[转帖] 时区的坑,别再踩了!

时区 · 浏览次数 : 0

小编点评

#DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV"); #Instant instant = Instant.ofEpochSecond(515527200); //ZonedDateTime time1 = ZonedDateTime.parse("1986-05-04 02:00:00 Asia/Shanghai", dtf); //ZonedDateTime time2 = ZonedDateTime.parse("1986-09-14 01:00:00 Asia/Shanghai", dtf); //ZonedDateTime time1 = ZonedDateTime.parse("1986-05-04 01:00:00 Asia/Shanghai", dtf); //ZonedDateTime time2 = ZonedDateTime.parse("1986-09-14 01:00:00 Asia/Shanghai", dtf); //printZonedDateTime(time1); //printZonedDateTime(time2); //printZonedDateTime(time1.plusHours(1)); //printZonedDateTime(time2.plusHours(1)); //printZonedDateTime(time2.plusHours(1)); //Instant instant = Instant.ofEpochSecond(515527200); //System.out.println(ZoneId.of("Asia/Shanghai").getRules().getOffset(instant)); //输出+08:00 //Instant instant = Instant.ofEpochSecond(515527200); //System.out.println(ZoneId.of("Asia/Shanghai").getRules().getOffset(instant));

正文

https://www.cnblogs.com/codelogs/p/16027236.html

 

原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。

简介#

最近在使用date命令时,发现表示东8区(中国时区)要使用GMT-8,但在Java中却需要使用GMT+8,如下:

$ TZ='GMT-8' date -d@1647658144 +'%F %T %:z'
2022-03-19 10:49:04 +08:00

# 如果用GMT+8,反而慢了16小时
$ TZ='GMT+8' date -d@1647658144 +'%F %T %:z'
2022-03-18 18:49:04 -08:00

而在Java中,如下:

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss XXX");
String dateStr = dtf.format(Instant.ofEpochSecond(1647658144).atZone(ZoneId.of("GMT+8")));
System.out.println(dateStr);
//输出2022-03-19 10:49:04 +08:00

这就让人有点迷糊了,经过一段时间搜索,发现在时区表达形式上还有不少知识点呢!

时区的偏移量表示法#

众所周知,为了方便各地区本地时间之间的转换,人们将全球划分为了24个时区,以格林尼治天文台(GMT)为零时区,往东西两个方向分别有12个时区,所以自然有了以GMT为前缀的时区表示法,如下:

GMT+8表示东8区,中国就是使用这个时区,而GMT-8表示西8区,如果格林尼治天文台的本地时间是2022-03-19的0点,那么GMT+8地区的本地时间就是2022-03-19的8点,而GMT-8的本地时间就是往前8小时,即2022-03-18的16点。

注意,上面的各地区本地时间的表述虽然不同,但它们实际是同一个时刻(绝对时间),要理解本地时间与绝对时间的区别。

GMT+8正是Java中支持的时区表示法,那为啥Linux中却是GMT-8呢?实际上Linux中的GMT-8也可以写成Etc/GMT-8,这才是它的标准名称,如下:

$ TZ='Etc/GMT-8' date -d@1647658144 -Is
2022-03-19T10:49:04+08:00
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss XXX");
String dateStr = dtf.format(Instant.ofEpochSecond(1647658144).atZone(ZoneId.of("Etc/GMT-8")));
System.out.println(dateStr);
//输出2022-03-19 10:49:04 +08:00

可以发现用Etc/GMT-8的话,Linux与Java的输出都是一样的了,是的,Etc/GMT-8也是一种类似GMT+8的时区表示机制,只不过它的+-号是反的。

Ok,虽然上面的差异弄清楚了,但时区的表示形式还没有介绍完,接着往下看...

除了GMT+8表示方式外,我们还经常会看到UTC+8这样的表示方式,这是UTC时区表示法。

即生GMT何生UTC?这是由于GMT是以格林尼治天文台为时间基准,但地球不是完美球体且自转速度在变慢,所以地球自转速度并不均匀,这导致以格林尼治天文台为时间基准是不准的。

为了更准确度量时间,科学家们发明了UTC时间,以铯原子跃迁次数来度量时间,比GMT时间更准确,为了保证GMT的准确性,每隔几年GMT时间会做一次调整,以与UTC时间对齐。

因此,既然有了更准确的UTC,那么就有了以UTC为前缀的时区表示法,如中国时区可使用UTC+8

各时区偏移量表示法一览表,如下:

偏移量表示法描述
GMT+8 相对GMT多8个小时
Etc/GMT-8 同GMT+8,+-号相反
UTC+8 同GMT+8
GMT+08:00 精确到分钟级别
GMT+08:00:00 精确到秒级别
GMT+0800 精确到分钟级别,省略冒号
GMT+080000 精确到秒级别,省略冒号
+08:00 精确到分钟级别,省略前缀
+08:00:00 精确到秒级别,省略前缀
+0800 精确到分钟级别,省略前缀与冒号
+080000 精确到秒级别,省略前缀与冒号
Z 表示零时区,等同于GMT、UTC、GMT+0、UTC+0

时区的区域表示法#

除了用偏移量来表示时区,为了方便,人们还按区域/城市的方式来定义时区,如Asia/ShanghaiAsia/Hong_Kong都表示东8区,具体有哪些城市命名的时区,可以在时区数据库中查看。

另外,为了简化区域时区表示法,又定义了一套时区缩写,如CST是中国时区China Standard Time的缩写,可以在时区缩写中查看各种缩写定义。

注意,一般都不建议使用时区缩写,因为时区缩写的命名经常会重复,比如CST是Central Standard Time(北美中部标准时间UTC -6)、China Standard Time(中国标准时间UTC +8)、Cuba Standard Time(古巴标准时间UTC -5)。

由于不同软件对CST的解释可能不同,导致会出现时间相差13或14个小时的情况,这在Java搭配MySQL时经常出现,我还专门写了一篇文章mysql的timestamp会存在时区问题?,对于一定要使用时区缩写的场景,可以使用香港时区缩写HKT,它不重复且和上海处于同一个时区。

区域表示法描述
Asia/Shanghai 上海时区,即东8区
CST 时区缩写,慎用

Java中表示时区#

在Java中和时区相关的类有TimeZone、ZoneId,其中TimeZone是老的时区类,而ZoneId是新的时区类,它有ZoneOffset和ZoneRegion两个子类,分别代表偏移量表示法和区域表示法。

那它们都支持上述的哪些时区写法呢?写个Demo验证一下,如下:

public static void main(String[] args) {
	printZoneId("+08:00");
	printZoneId("+0800");
	printZoneId("GMT+8");
	printZoneId("Etc/GMT-8");
	printZoneId("UTC+8");
	printZoneId("Asia/Shanghai");
	printZoneId("CST");
	printZoneId("Z");
}

public static void printZoneId(String zone){
	ZoneId zoneId;
	if(!ZoneId.SHORT_IDS.containsKey(zone)){
		zoneId = ZoneId.of(zone);
	}else{
		zoneId = ZoneId.of(ZoneId.SHORT_IDS.get(zone));
	}
	TimeZone timeZone = TimeZone.getTimeZone(zone);
	ZoneOffset zoneOffset = zoneId.getRules().getOffset(Instant.now());
	DateTimeFormatter dtf = DateTimeFormatter.ofPattern("xxx ZZZ O OOOO");
	System.out.printf("%-14s -> %-28s -> class:%s -> TimeZone.offset:%d \n", zone, dtf.format(zoneOffset),
			zoneId.getClass().getSimpleName(), timeZone.getRawOffset());
}

输出如下:

+08:00         -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneOffset -> TimeZone.offset:0 
+0800          -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneOffset -> TimeZone.offset:0 
GMT+8          -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneRegion -> TimeZone.offset:28800000 
Etc/GMT-8      -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneRegion -> TimeZone.offset:28800000 
UTC+8          -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneRegion -> TimeZone.offset:0 
Asia/Shanghai  -> +08:00 +0800 GMT+8 GMT+08:00 -> class:ZoneRegion -> TimeZone.offset:28800000 
CST            -> -05:00 -0500 GMT-5 GMT-05:00 -> class:ZoneRegion -> TimeZone.offset:-21600000 
Z              -> +00:00 +0000 GMT GMT         -> class:ZoneOffset -> TimeZone.offset:0 
时区写法ZoneIdTimeZone
+08:00 支持 不支持
+0800 支持 不支持
GMT+8 支持 支持
Etc/GMT-8 支持 支持
UTC+8 支持 不支持
Asia/Shanghai 支持 支持
CST 支持,代表北美西部时间,非中国标准时间 支持,代表北美西部时间,非中国标准时间
Z 支持 支持

偏移量表示法与区域表示法区别#

虽然偏移量表示法与区域表示法都可以表示时区,但由于夏令时的存在,它们并不完全等同。

夏令时(Daylight Saving Time: DST),也叫 夏时制,是指为了节约能源,在天亮的早的夏季,人为将时间调快一小时,以充分利用光照资源,节约照明用电。

而中国在 1986 年至 1991 年也实行过夏令时,在1986~1991的每年从四月中旬第一个星期日的凌晨2时整(北京时间),将时钟拨快一小时,即将表针由2时拨至3时,夏令时开始;到九月中旬第一个星期日的凌晨2时整(北京夏令时),再将时钟拨回一小时,即将表针由2时拨至1时,夏令时结束。从1986年到1991年的六个年度,除1986年因是实行夏时制的第一年,从5月4日开始到9月14日结束外,其它年份均按规定的时段施行。

故会有下面看起来有点奇怪的现象:

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
Instant instant = Instant.ofEpochSecond(515527200);
System.out.println(dtf.format(instant.atZone(ZoneId.of("Asia/Shanghai"))));
//输出1986-05-04 03:00:00 Asia/Shanghai
System.out.println(dtf.format(instant.atZone(ZoneId.of("GMT+8"))));
//输出1986-05-04 02:00:00 GMT+08:00

为什么Asia/Shanghai输出为3点,而GMT+8输出为2点呢?原因是1986-05-04 02:00:00这个时间点中国正开始实行夏令时,时钟拨快了1小时。

GMT+8为什么输出为2点呢?因为中国、马来西亚、菲律宾、新加坡的时区都是GMT+8,只有中国在实行夏令时,而在GMT+8中没法感知到区域信息,那java只能以没有实行夏令时的方法来计算本地时间了。

夏令时导致的奇怪现象#

正是由于夏令时的存在,导致程序可能出现诡异的现象甚至bug,如下:

  1. 由于夏令时会将2点改成3点,导致2点没了,所以date命令报错了
$ TZ='Asia/Shanghai' date -d 1986-05-04T02:00:00 +%s
date: invalid date ‘1986-05-04T02:00:00’

$ TZ='Asia/Shanghai' date -d 1986-05-04T03:00:00 +%s
515527200
  1. 时间解析后再格式化输出,发现不一样了
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
ZonedDateTime time1 = ZonedDateTime.parse("1986-05-04 02:00:00 Asia/Shanghai", dtf);
System.out.println(time1.format(dtf));
//输出1986-05-04 03:00:00 Asia/Shanghai
  1. 时间加1小时,发现加了2小时或根本没变
public static void main(String[] args) {
	DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
	//加1小时刚好夏令时开始
	ZonedDateTime time1 = ZonedDateTime.parse("1986-05-04 01:00:00 Asia/Shanghai", dtf);
	printZonedDateTime(time1);
	printZonedDateTime(time1.plusHours(1));
	
	//加1小时刚好夏令时结束
	ZonedDateTime time2 = ZonedDateTime.parse("1986-09-14 01:00:00 Asia/Shanghai", dtf);
	printZonedDateTime(time2);   			
	printZonedDateTime(time2.plusHours(1));   
}

private static void printZonedDateTime(ZonedDateTime time){
	DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
	System.out.println(time.format(dtf));
}

输出如下:

1986-05-04 01:00:00 Asia/Shanghai
1986-05-04 03:00:00 Asia/Shanghai  //加1小时,结果看起来加了2个小时
1986-09-14 01:00:00 Asia/Shanghai
1986-09-14 01:00:00 Asia/Shanghai  //加1小时,结果时间看起来没变

为啥会这样呢?原因是本地时间虽然看起来没变,但Asia/Shanghai这个代表的时区却发生了变化。

我们可以将上面printZonedDateTime中时间格式由yyyy-MM-dd HH:mm:ss VV修改为yyyy-MM-dd HH:mm:ss VV xxx再执行,发现输出如下:

1986-05-04 01:00:00 Asia/Shanghai +08:00
1986-05-04 03:00:00 Asia/Shanghai +09:00
1986-09-14 01:00:00 Asia/Shanghai +09:00
1986-09-14 01:00:00 Asia/Shanghai +08:00

如上,夏令时导致Asia/Shanghai这个时区不一定是东8区了,也可能是东9区,故Java中,想将ZoneRegion转换为ZoneOffset,需要传递一个instant时刻参数,如下:

//输出+08:00
Instant instant = Instant.now();
System.out.println(ZoneId.of("Asia/Shanghai").getRules().getOffset(instant));
//输出+09:00,在1986-05-04 02:00:00 +08:00处于夏令时,增加了1小时
Instant instant = Instant.ofEpochSecond(515527200);
System.out.println(ZoneId.of("Asia/Shanghai").getRules().getOffset(instant));

夏令时真是一种自欺欺人的做法,还好中国从1991年后就没再实行了!

作者:打码日记

出处:https://www.cnblogs.com/codelogs/p/16027236.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

与[转帖] 时区的坑,别再踩了!相似的内容:

[转帖] 时区的坑,别再踩了!

https://www.cnblogs.com/codelogs/p/16027236.html 原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。 简介# 最近在使用date命令时,发现表示东8区(中国时区)要使用GMT-8,但在Java中却需要使用GMT+8,如下:

[转帖]【JVM】类加载机制

什么是类的加载 将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供

[转帖]JVM 运行数据区深度解析

https://my.oschina.net/jiagoushi/blog/5597878 运行数据区 字节码只是一个二进制文件存放在那里。要想在 jvm 里跑起来,先得有个运行的内存环境。 也就是我们所说的 jvm 运行时数据区。 1)运行时数据区的位置 运行时数据区是 jvm 中最为重要的部分,

[转帖]JVM类加载机制

概述 虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Cl

【转帖】47.直接内存

目录 1.直接内存概述2.`IO`与`NIO`对比3.直接内存的`OOM`与内存大小设置 1.直接内存概述 1.直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。 2.直接内存是在Java堆外,直接向系统申请的内存空间 3.Java的NIO库允许使用直接内存,用于数据

[转帖]使用Rclone实现minio数据的迁移

使用Rclone实现minio数据的迁移 一、准备 1.1 使用工具 rclone:开源的对象存储在线迁移工具,用于文件和目录的同步,支持阿里云的oss、minio 、亚马逊S3 等。 1.2 注意事项 1、两台机器的时区及时间要保持一致,最后进行迁移之前,两台机器的时间进行校准。方法如下: #ce

[转帖] mysql的timestamp会存在时区问题?

我感觉 这样理解也有点不对 timestamp 应该是不带时区 只是 UTC1970-1-1 的时间戳 但是展示时会根据时区做一下计算 date time 就不会做转换而已. 原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。 简介# 众所周知,mysql中有两个时间类型

[转帖]12.JVM运行时数据区之虚拟机栈概述

`https://blog.csdn.net/u011069294/article/details/107050001` 目录 1. 内存中的栈与堆2.栈的优点 1. 内存中的栈与堆 栈是运行时单位,堆是存储的单位。 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。 堆解决的是数据存储的问

[转帖]docker容器自动重启,看完这篇彻底明白了

一. JVM内存区域的划分 1.1 java虚拟机运行时数据区 java虚拟机运行时数据区分布图: JVM栈(Java Virtual Machine Stacks): Java中一个线程就会相应有一个线程栈与之对应,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈,因此栈存储的信息都是跟当

[转帖]【JVM】Java内存区域与OOM

引入 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。 Java虚拟机运行时数据区 如图所示 1.程序计数器(线程私有) 作用 记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节