丢失的8小时去哪里了?SimpleDateFormat线程不安全,多线程初始化异常解决方案

简介: 丢失的8小时去哪里了?SimpleDateFormat线程不安全,多线程初始化异常解决方案

前言

本次参加了2月份的征文活动,说是对时间的处理问题,我这有2个点需要分享一下,一个是上大学的时候碰到的,还有就是在工作中遇到的由于【SimpleDateFormat线程不安全】在多线程时间初始化的时候遇到的异常问题以及解决方案。希望能为大家创造一些价值。



1、丢失的8小时去哪里了

       在当年大二选修课的时候就遇到了这个问题,是时间戳转换成时间的时候,如果是自己来计算则会少了8个小时,用给定的函数就直接出来了,当时又不会配置查看源码,很苦恼,如果当时知道CSDN就好了,直接就能收到,所以当时是一直不知道为啥,后来问老师才知道的,我们今天就再来算一算,在这个分析过程中来看看具体是因为什么。

Java计算时间戳转换当前时分秒

Date date = new Date();
// 获取当前的时间戳·单位毫秒·21时15分32秒
long nowTime = date.getTime();

输出时间戳:这个时间戳的单位是毫秒。

在这里我们是根本看不出来是为啥的。接下来我们来一个一个的计算。

转换成秒计算小时

毫秒换算成秒

long second = nowTime / 1000;

换算成当前秒

long seconds = second % 60;

换算成当前分钟

long minutes = second / 60 % 60;

换算成小时

long hours = minutes / 60 % 24;

我们可以获取到:

很明显,我们计算的小时是有问题的,这个时间戳的时间是:【21时15分32秒】。可是时间换算完毕是13时,很明显21-13=8,相差8个小时,这个时候我们就很懵逼,咋回事呢?

我记得很早以前,我还只会VB语言的时候就遇到过这个问题。后来老师说,咱们是东八区我一下就明白了。

原来我们在东八区,所以我们的地区时应该在这个时间戳的基础上加上8个小时就对了。故而有以下代码:

package com.item.action;
import java.util.Date;
public class Main {
  public static void main(String[] args) {
    // TODO Auto-generated method stub
//    Date date = new Date();
//    // 获取当前的时间戳·单位毫秒
//    long nowTime = date.getTime();
//    System.out.println(nowTime);
    long nowTime = 1676985332143l;
    // 获取秒
    long second = nowTime / 1000;
    System.out.println(second + "s");
    // 获取分钟
    long seconds = second % 60;
    System.out.println(seconds + "s");
    long minutes = second / 60;
    System.out.println(minutes % 60 + "min");
    // 获取小时
    long hours = minutes / 60 % 24 + 8;
    System.out.println(hours + "h");
    System.out.println(hours+"时"+(minutes % 60)+"分"+seconds+"秒");
  }
}

输出效果:

补上8个小时就是正确的时间了,这个类型题在蓝桥杯上也是出现过的,大家可以搜一搜,前三题,那个题目我倒是忘记了。

最后对于这个问题我们来看看百度上怎么说的:

东八区(UTC/GMT+08:00)是比世界协调时间(UTC)/格林尼治时间(GMT)快8小时的时区。

你看看,有了这句话就一下豁然开朗,我们计算的事件就是少了8个小时,所以加上就行了。

这里我找到了地区时的列表,可供大家查阅:

UTC-12: 国际日期变更线以西12小时

UTC-11: 标准时间时减去11小时

UTC-10: 标准时间时减去10小时

UTC-9: 标准时间时减去9小时

UTC-8: 标准时间时减去8小时

UTC-7: 标准时间时减去7小时

UTC-6: 标准时间时减去6小时

UTC-5: 标准时间时减去5小时

UTC-4: 标准时间时减去4小时

UTC-3: 标准时间时减去3小时

UTC-2: 标准时间时减去2小时

UTC-1: 标准时间时减去1小时

UTC: 标准时间·也就是全世界约定好的时间点

UTC+1: 标准时间时加上1小时

UTC+2: 标准时间时加上2小时

UTC+3: 标准时间时加上3小时

UTC+4: 标准时间时加上4小时

UTC+5: 标准时间时加上5小时

UTC+6: 协标准时间加上6小时

UTC+7: 标准时间时加上7小时

UTC+8: 标准时间时加上8小时

UTC+9: 标准时间时加上9小时

UTC+10: 标准时间时加上10小时

UTC+11: 标准时间时加上11小时

UTC+12: 标准时间时加上12小时

根据自己国家的地区时来进行加减就能得到自己本国的具体时间了。

2、SimpleDateFormat线程不安全,多线程初始化异常解决方案

这个问题挺坑的,只要多线程获取的时候就会格式化失败,复现代码不复杂,我就直接写了一个,并且没有进行大规模测试。

异常复现

这里直接new一个SimpleDateFormat就行,但是一定要给个static,这样就防止了各种new出现对内存的消耗

异常示例代码:

可以直接复制用来自己的测试,我没有写包名,方便大家。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Demo {
  /**
   * 项目中·为了防止无限new就设置的静态变量
   */
  static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  public static Date parseToDate(String timeStr) throws ParseException {
    return format.parse(timeStr);
  }
  public static void main(String[] args) {
    // TODO Auto-generated method stub
    /**
     * 我们不多测试,仅仅小小的多线程处理,就3个。
     */
    for (int i = 0; i < 3; i++) {
      new Thread() {
        @Override
        public void run() {
          try {
            System.out.println(parseToDate("2023-02-23 08:25:38"));
          } catch (ParseException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          }
        }
      }.start();
    }
  }
}

异常输出效果

很直接的看到,第一个肯定是输出成功了的,但是后面获取的时候就出现了异常,但是它直说FOR input string输入字符串,但是我们不知道底层是啥的话很难分析为啥,所以我们得对源码进行分析一下。

异常源码分析

首先我们来看SimpDateFormat的类,能看到是直接继承了DateFormat,我们要分析的就是它们两个。

我们看看SimpDateFormat类里面重写了DateFormat中的parse函数。

代码中我们能看到【CalendarBuilder】这个对象,在1532行能看到是通过【establish】将【calendar】设置到属性中,在1537行能看到具体的操作。

calendar是父类DateFormat类的共享变量,可以被多个线程访问到

所以,当SimpleDateFormat被声明为static的时候时就是线程不安全的,多个线程同时操作,肯定会显现访问异常。

通过查看format()发现有一行calendar.setTime(date);操作的是共享变量calendar,那这么一看就很明显了——线程不安全。

具体解决方案示例(100%解决,自己项目就这么用的)

这里在解决的时候我们使用【DateTimeFormatter】来解决,但是这个类在JDK1.8之后才会有,这里声明一下,是【JAVA_JDK1.8】,【LocalDateTime】本身绝对是线程安全的,那么我们就可以直接使用它来替换,看下面的代码就能看到,但是时间的类型这里需要使用【LocalDateTime】来处理。

package test;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Demo1 {
  static final DateTimeFormatter dtformat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  public static LocalDateTime parseToDate(String timeStr) {
    return LocalDateTime.parse(timeStr,dtformat);
  }
  public static void main(String[] args) {
    // TODO Auto-generated method stub
    /**
     * 既然我们做了处理,那么我们就来300个线程试一试<br/>
     * 开两个循环一个循环150次。
     */
    for (int i = 0; i < 150; i++) {
      new Thread() {
        @Override
        public void run() {
          System.out.println(parseToDate("2023-02-23 08:25:38"));
        }
      }.start();
    }
    for (int i = 0; i < 150; i++) {
      new Thread() {
        @Override
        public void run() {
          System.out.println(parseToDate("2023-02-23 08:25:38"));
        }
      }.start();
    }
  }
}

无异常效果

到这里,我们就彻底解决了这个项目部署的时候遇到时间初始化的问题。

希望能对初学者有一定的帮助,很多项目在初始化的时候都是要使用到时间,那么当出现问题的时候大概都是这个问题,使用我的方法就能迎刃而解了。

总结

我们初学的孩子们肯定都是在使用SimpleDateFormat来格式化时间,很少会遇到多线程的时候,所以是根本没有感觉的,但是这个bug是客观存在的,在class中我们甚至看到了开发者留的注释信息,虽然是对源码进行分析找到了原因,但是想彻底的更换固有思维逻辑还是需要一定的时间的,包括我在内,最开始在企业搞开发的时候还好,遇到大型初始化的不多,但是后来自己开始掌控项目后发现,真的有很多你开发的时候不会注意到是事情就都出现了,毕竟你已经开始统筹全局。以后遇到时间格式化的时候使用【DateTimeFormatter】来格式化,线程安全,保证不会出现这类异常了。

相关文章
|
11月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
436 0
|
缓存 Java
线程池初始化严禁使用Executors
线程池初始化严禁使用Executors
|
11月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
12月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
793 5
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
578 20
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
826 14
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
11月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
468 83
|
8月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
307 6

热门文章

最新文章