【SimpleDateFormat】线程不安全问题分析及解决方案

news2024/11/20 14:20:51

前言

在日常开发中,我们经常需要去做日期格式转换,可能就会用到SimpleDateFormat类。但是,如果使用不当,就很容易引发生产事故

1. 问题推演

1.1 初始日期工具类

刚开始的日期转换工具类可能长这样:

public class DateUtil {
  public static String formatDate(Date date) {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    return sdf.format(date);
  }
}

1.2 引入线程安全问题

这时候,就有人要说了,以上的代码存在问题,每次调用的使用,都要创建SimpleDateFormat,在频繁使用时,就会创建大量的对象。

所以将代码改造成了这样:

public class DateUtil {
  private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  public static String formatDate(Date date) {
    return sdf.format(date);
  }
}

在这里,看似优化了性能,不管被调用多少次,都只有一个SimpleDateFormat对象,但是却引入了线程安全问题

1.3 并发问题示例

public class TestDateUtil {
  public static void main(String[] args) throws InterruptedException {
    // 创建线程池
    ExecutorService executorService = Executors.newFixedThreadPool(10);

    Date date1 = new Date(3600);
    Date date2 = new Date(36000);

    // 调用次数
    int n = 10;
    for (int i = 0; i < n; i++) {
      int finalI = i;
      executorService.execute(() -> {
        if (finalI % 2 == 0) {
          System.out.println("Date为:" + date1 + " 转换结果为:" + DateUtil.formatDate(date1));
        } else {
          System.out.println("Date为:" + date2 + " 转换结果为:" + DateUtil.formatDate(date2));
        }
      });
    }
    // 等待执行结果
    executorService.shutdown();
  }
}

输出结果:

Date为:Thu Jan 01 08:00:03 CST 1970 转换结果为:1970-01-01 08:00:03
Date为:Thu Jan 01 08:00:36 CST 1970 转换结果为:1970-01-01 08:00:36
Date为:Thu Jan 01 08:00:03 CST 1970 转换结果为:1970-01-01 08:00:03
Date为:Thu Jan 01 08:00:36 CST 1970 转换结果为:1970-01-01 08:00:03 // 错误结果
Date为:Thu Jan 01 08:00:36 CST 1970 转换结果为:1970-01-01 08:00:03 // 错误结果
Date为:Thu Jan 01 08:00:36 CST 1970 转换结果为:1970-01-01 08:00:03 // 错误结果
Date为:Thu Jan 01 08:00:36 CST 1970 转换结果为:1970-01-01 08:00:03 // 错误结果
Date为:Thu Jan 01 08:00:03 CST 1970 转换结果为:1970-01-01 08:00:36 // 错误结果
Date为:Thu Jan 01 08:00:03 CST 1970 转换结果为:1970-01-01 08:00:36 // 错误结果
Date为:Thu Jan 01 08:00:03 CST 1970 转换结果为:1970-01-01 08:00:03

可以看到上方出现了各种转换问题,【Thu Jan 01 08:00:36 CST 1970】的数据被转换成了【1970-01-01 08:00:03】。

1.4 阿里巴巴规范

阿里巴巴规范也提出,不要SimpleDateFormat定义为static变量

image-20231003235317342

2. 问题分析

查看源码,分析问题。

image-20231003225715789

image-20231003230025194

因为在SimpleDate类中,使用了成员变量在方法中进行传参调用,在多线程之间并发set、get中,很容易就产生了线程安全问题。

3. 解决方法

3.1 使用局部变量

使用局部变量,即最开始的用法,每一次都创建自己的SimpleDateFormat对象,即可解决并发问题

public class DateUtil {
  public static String formatDate(Date date) {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    return sdf.format(date);
  }
}

缺点:在高并发情况下会创建很多的对象,不推荐。

3.2 synchronized锁

使用synchronized对存在线程安全的代码块进行同步处理

public class DateUtil {
  private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  public static String formatDate(Date date) {
    synchronized (sdf) {
      return sdf.format(date);
    }
  }
}

缺点:同一个时刻,只能有个一个线程执行format方法,性能比较差

3.3 ThreadLocal方式

使用ThreadLocal每个线程持有自己的SimpleDateFormat,解决多线程之间并发问题

public class DateUtil {
  // 创建 ThreadLocal 对象,并设置默认值(new SimpleDateFormat)
  private static ThreadLocal<SimpleDateFormat> threadLocal =
      ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

  public static String formatDate(Date date) {
      return threadLocal.get().format(date);
  }
}

3.4 使用DateTimeFormatter

以上方案都是因为SimpleDateFormat线程不安全导致我们需要去特殊处理,但在JDK 8之后,可以直接使用线程安全类DateTimeFormatter

使用 DateTimeFormatter 必须要配合 JDK 8 中新增的时间对象 LocalDateTime 来使用,因此在操作之前,我们可以先将 Date 对象转换成 LocalDateTime,然后再通过 DateTimeFormatter 来格式化时间,具体实现代码如下:

public class DateUtil {
  // 创建 DateTimeFormatter 对象
  private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

  public static String formatDate(Date date) {
    // 将 Date 转换成 JDK 8 中的时间类型 LocalDateTime
    LocalDateTime localDateTime =
        LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
      return dateTimeFormatter.format(localDateTime);
  }
}

4. 各方案优缺点总结

如果是使用JDK 8+,则直接使用DateTimeFormatter即可。如果使用的是低版本的JDK,则可以使用TheadLocalsynchronized解决方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1059058.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nodejs+vue晓海网上订餐系统elementui

管理员功能需求 管理员登陆后&#xff0c;主要模块包括首页、个人中心、用户管理、菜单信息管理等功能。 第三章 系统分析 10 3.1需求分析 10 3.2可行性分析 10 3.2.1技术可行性&#xff1a;技术背景 10 3.2.2经济可行性 11 3.2.3操作可行性&#xff1a; 11 3.3性能分析 11 3.4…

创建GCP service账号并管理权限

列出当前GCP项目的所有service account 我们可以用gcloud 命令 gcloud iam service-accounts list gcloud iam service-accounts list DISPLAY NAME EMAIL DISABLED terraform …

数据结构 1.1 初学数据结构

数据结构的基本概念 数据结构在学什么&#xff1f; 如何用程序代码把现实世界的问题信息化 如何用计算机高效处理信息从而创造价值 数据&#xff1a; 数据元素、数据项&#xff1a; 数据元素——描述一个个体 数据对象——数据元素之间具有同样的性质 同一个数据对象里的数…

BGP高级特性——4字节AS号

目录 4字节AS号 相关概念 两种过渡属性 4字节AS号的格式 4字节AS号建立邻居 4字节AS号路由传递 配置命令 4字节AS号 相比于2字节AS号&#xff0c;范围更大。由1~65535扩展到1~4294967295 支持4字节AS号的BGP设备兼容仅支持2字节AS号的BGP设备 相关概念 Speaker&#…

13链表-简单思路练习

目录 LeetCode之路——876. 链表的中间结点 分析&#xff1a; 解法一&#xff1a;常规思路 解法二&#xff1a;快慢指针 LeetCode之路——876. 链表的中间结点 给你单链表的头结点 head &#xff0c;请你找出并返回链表的中间结点。 如果有两个中间结点&#xff0c;则返回…

详解Linux的系统调用fork()函数

在Linux系统中&#xff0c;fork()是一个非常重要的系统调用&#xff0c;它的作用是创建一个新的进程。具体来说&#xff0c;fork()函数会在当前进程的地址空间中复制一份子进程&#xff0c;并且这个子进程几乎完全与父进程相同&#xff0c;包括进程代码、数据、堆栈以及打开的文…

利用Redis实现全局唯一ID

利用Redis实现全局唯一ID 背景 场景分析&#xff1a;如果我们的id具有太明显的规则&#xff0c;用户或者说商业对手很容易猜测出来我们的一些敏感信息&#xff0c;比如商城在一天时间内&#xff0c;卖出了多少单&#xff0c;这明显不合适。 场景分析二&#xff1a;随着我们商…

怎么将本地代码文件夹通过Git 命令上传到启智平台仓库

在本地创建一个与启智平台仓库同样名字的文件夹 然后在本地文件夹右键–>选择Git Bash Here,就会打开Git命令窗口 初始化本地仓库 git init将项目文件添加到Git git add .提交更改&#xff1a; 使用以下命令提交您的更改&#xff0c;并为提交添加一条描述性的消息&#…

ArcGIS Engine:实现Shp/Mxd数据的加载、图层的简单查询

本博客参考&#xff1a;BiliBili UP主 <羊羊旸> &#xff1a; Arcgis Engine学习 目录 01 加载控件以及控件的基本信息等调整 02 编写 <菜单-地图控件> 中各个子工具的代码 2.1 加载Shapefile数据-代码 2.2 加载地图文档数据-代码 2.3 获取图层数量-代码 2.…

lv7 嵌入式开发-网络编程开发 04 IP地址与端口号

目录 1 IP地址 1.1 IP 地址及其表示方法 1.2 分类的 IP 地址 1.3 无分类编址 CIDR 1.3.1 网络前缀 1.3.2 地址块 1.3.3 地址掩码 (address mask) 1.4 IPv6 的地址 1.4.1 表示方式 1.4.2 零压缩 2 端口号 2.1 进程之间的通信 2.2 运输层的作用 2.3 屏蔽作用 2.4…

CompletableFuture 异步编排

目录 CompletableFuture 的详解代码测试配置类的引入Demo1Demo2CompletableFuture的async后缀函数与不带async的函数的区别ThreadPoolTaskExecutor 和 ThreadPoolExecutor 的区别Spring 线程池的使用业务使用多线程的原因场景一:场景二:FutureTask介绍线程池为什么要使用阻塞队…

(三) Markdown插入互联网或本地视频解决方案

前言 不论博客系统是WordPress还是Typecho&#xff0c;绕不开的是两种书写语言&#xff0c;一种称之为富文本&#xff0c;一种叫做Markdown。 Markdown有很多好处&#xff0c;也有很多坏处&#xff0c;比如Markdown本身不具备段落居中的功能&#xff0c;以及Markdown也不具有…

聊聊JAVA中的锁优化锁升级及其底层原理剖析

文章目录 1. 基础介绍java中的锁什么是锁优化和锁升级 2. Java中的锁升级过程及底层原理1. 偏向锁实现机制和原理1. 偏向锁的原理2. 偏向锁的底层实现 2. 轻量级锁1. 轻量级锁的原理2. 轻量级锁的底层实现 3. 重量级锁1. 重量级锁的原理2. 重量级锁的底层实现 3. Java中锁升级的…

selenium +IntelliJ+firefox/chrome 环境全套搭配

1第一步&#xff1a;下载IntelliJ idea 代码编辑器 2第二步&#xff1a;下载浏览器Chrome 3第三步&#xff1a;下载JDK 4第四步&#xff1a;配置环境变量&#xff08;1JAVA_HOME 2 path&#xff09; 5第五步&#xff1a;下载Maven 6第六步&#xff1a;配置环境变量&#x…

新环境下配置jupyter notebook并启动

1.先切换到我们需要使用的环境 2.安装python内核 conda install ipykernel 3.创建指定内核 ipython kernel install --user --nametensorflow2 4.运行jupyter notebook 即出现新创建的内核

计算机网络 第一章计算机网络体系结构

非常好的课程。计算机网络 //----------------------------------------------------------------------------------------------------------------// 需要清楚实体、协议、服务。 网络协议三要素&#xff0c;语法、语义、同步。

遗留系统陷入困境

当我们谈论遗留系统时&#xff0c;我们经常会想到数据中心某处的过时服务器和交换机。我们带着一种病态的迷恋阅读了有关系统性技术问题的文章&#xff0c;这些问题在假期周末困扰着其他旅行者&#xff0c;并为他们缺乏远见而摇头。 然后&#xff0c;我们坐在屏幕前&#xff0…

数据结构—栈、队列、链表

一、栈 Stack&#xff08;存取O(1)&#xff09; 先进后出&#xff0c;进去123&#xff0c;出来321。 基于数组&#xff1a;最后一位为栈尾&#xff0c;用于取操作。 基于链表&#xff1a;第一位为栈尾&#xff0c;用于取操作。 1.1、数组栈 /*** 基于数组实现的顺序栈&#…

4 Ways to Fix an Operation Did Not Complete Virus Error on Windows

文章目录 Can’t open a file on your PC? Use these methods to fix your issue. A Windows operation did not complete virus error.Mahesh Makvana / How-To Geek Readers like you help support How-To Geek. When you make a purchase using links on our site, we ma…

Spring实例化源码解析之registerBeanPostProcessors(六)

BeanPostProcessors是Spring框架中的一个扩展机制&#xff0c;它允许开发人员在Spring容器实例化、配置和初始化Bean的过程中干预和定制化。BeanPostProcessor接口定义了两个方法&#xff1a;postProcessBeforeInitialization和postProcessAfterInitialization&#xff0c;分别…