【Java进阶篇】SimpleDateFormat是线程安全的吗? 使用时应该注意什么?

news2025/1/23 5:01:09

在这里插入图片描述

SimpleDateFormat是线程安全的吗?使用时应该注意什么?

  • ✔️ 典型解析
  • ✔️拓展知识仓
    • ✔️SimpleDateFormat用法
    • ✔️日期和时间模式表达方法
    • ✔️输出不同时区的时间
    • ✔️SimpleDateFormat线程安全性
    • ✔️问题重现
    • ✔️线程不安全原因
    • ✔️如何解决
      • ✔️使用局部变量
      • ✔️加同步锁
      • ✔️使用ThreadLocal
      • ✔️使用DateTimeFormatter


✔️ 典型解析


在日常开发中,我们经常会用到时间,我们有很多办法在Java代码中获取时间。但是不同的方法获取到的时间的格式都不尽相同,这时候就需要一种格式化工具,把时间显示成我们需要的格式。


最常用的方法就是使用SimpleDateFormat类。这是一个看上去功能比较简单的类,但是,一旦使用不当也有可能导致很大的问题。


在阿里巴巴Java开发手册中,有如下明确规定:


在这里插入图片描述

也就是说SimpleDateFormat是非线程安全的,所以在多线程场景中,不能使用SimpleDateFormat作为共享变量。


因为SimpleDateFormat中的format方法在执行过程中,会使用一个成员变量calendar来保存时间。


如果我们在声明SimpleDateFormat的时候,使用的是static定义的。那么这个SimpleDateFormat就是一个共享变量,随之,SimpleDateFormat中的calendar也就可以被多个线程访问到。


假设线程1刚刚执行完 calendar.setTime 把时间设置成2018-11-11,还没等执行完,线程2又执行了 calendar.setTime 把时间改成了2018-12-12。这时候线程1继续往下执行,拿到的 calendar.setTime 得到的时间就是线程2改过之后的。


想要保证线程安全,要么就是不要把SDF设置成成员变量,只设置成局部变量就行了,要不然就是加锁避免并发,或者使用JDK 1.8中的DateTimeFormatter


✔️拓展知识仓


✔️SimpleDateFormat用法


SimpleDateFormat是Java提供的一个格式化和解析日期的工具类。它允许进行格式化 (日期->文本)、解析(文本->日期)和规范化。SimpleDateFormat 使得可以选择任何用户定义的日期-时格式的模式。


在Java中,可以使用SimpleDateFormatformat方法,将一个Date类型转化成String类型,并且可以指定输出格式。


//Date转String

Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.printIn(datastr);

以上代码,转换的结果是: 2018-11-25 13:00:00,日期和时间格式由 日期和时间模式 字符串指定。如果你想要转换成其他格式,只要指定不同的时间模式就行了。


在Java中,可以使用 SimpleDateForma parse 方法,将一个String 类型转化成Date类型。


//String转Data
System.out.println(sdf.parse(dataStr));

✔️日期和时间模式表达方法


在使用SimpleDateFormat的时候,需要通过字母来描述时间元素,并组装成想要的日期和时间模式。常用的时间元素和字母的对应表如下:


在这里插入图片描述

模式字母通常是重复的额,其数量确定其精确表示。如下表常用的输出格式的表示方法。


在这里插入图片描述

✔️输出不同时区的时间


时区是地球上的区域使用同一个时间定义。以前,人们通过观察太阳的位置(时角)决定时间,这就使得不同经度的地方的时间有所不同(地方时)。1863年,首次使用时区的概念。时区通过设立一区域的标准时间部分地解决了这个问题。


世界各个国家位于地球不同位置上,因此不同国家,特别是东西跨度大的国家日出、日落时间必定有所偏差。这些偏差就是所谓的时差。


现今全球共分为24个时区。由于使用上常常1个国家,或1个省份同时跨着2个或更多时区,为了照顾到行政上的方便,常将1个国家或1个省份划在一起。所以时区并不严格按南北直线来划分,而是按自然条件来划分。例如,中国幅员宽广,差不多跨5个时区,但为了使用方便简单,实际上在只用东八时区的标准时即北京时间为准。


由于不同的时区的时间是不一样的,甚至同一个国家的不同城市时间都可能不一样,所以,在Java中想要获取时间的时候,要重点关注一下时区问题。


默认情况下,如果不指明,在创建日期的时候,会使用当前计算机所在的时区作为默认时区,这也是为什么我们通过只要使用 new Date() 就可以获取中国的当前时间的原因。


那么,如何在Java代码中获取不同时区的时间呢? SimpleDateFormat 可以实现这个功能。


SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/Los Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));

以上代码,转换的结果是: 2018-11-24 21:00:00 。既中国的时间是11月25日的13点,而美国洛杉矶时间比中国北京时间慢了16个小时 (这还和冬夏令时有关系,就不详细展开了)


如果你感兴趣,你还可以尝试打印一下美国纽约时间(America/New York)。纽约时间是2018-11-25 00:00:00。纽约时间比中国北京时间早了13个小时。


当然,这不是显示其他时区的唯一方法,不过本文主要为了介绍SimpleDateFormat,其他方法暂不介绍了。


✔️SimpleDateFormat线程安全性


由于SimpleDateFormat比较常用,而且在一般情况下,一个应用中的时间显示模式都是一样的,所以很多人愿意使用如下方式定义SimpleDateFormat:


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

	public static void main(String[] args) {
		simpleDateFormat.setTimeZone(TimeZonegetTimeZone("America/New York"));
		System.out.println(simpleDateFormat.format(Calendar.getInstance().getTime())):
	}
}

这种定义方式,存在很大的安全隐患。


✔️问题重现


我们来看一段代码,以下代码使用线程池来执行时间输出。


/** @author xinbaobabaaibiancheng   */

public class Main {
	
	/**
	* 定义一个全局的simpleDateFormat
	*/
	private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

	/**
	*    使用ThreadFactoryBuilder定义一个线程池
	*/
	private static ThreadFactory namedThreadFactory = new   ThreadFactoryBuilder ().setNameFormat("demo-poo1-%d")build();

	private static ExecutorService pool = new ThreadPoolExecutor(5200,0LTimeUnit.MILLISECONDS,
	new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());


	/**
	*    定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行
	*/
	private static CountDownLatch countDownLatch = new CountDownLatch(100);

	public static void main(String[] args) {
		//定义一个线程安全的HashSet
		Set<String> dates = Collections.synchronizedSet(new HashSet<string>());
		for (int i = 0; i < 100; i++) {
			//获取当前时间
			Calendar calendar = Calendar.getInstance();
			int finalI = i;
			pool.execute(() -> {
				//时间增加
				calendar.add(Calendar.DATE. finalI);
				//通过simpleDateFormat把时间转换成字符串
				String dateString = simpleDateFormat.format(calendar.getTime());
				//把字符串放入Set中
				dates .add(datestring);
				//countDown
				countDownLatch .countDown();
		    });
	    }
	    //阻塞,直到countDown数量为0
	    countDownLatch.await();
	    //输出去重后的时间个数
		System.out.println(dates .size());
	}
}

以上代码,其实比较简单,很容易理解。就是循环一百次,每次循环的时候都在当前时间基础上增加个天数(这个天数随着循环次数而变化),然后把所有日期放入一个线程安全的、带有去重功能的Set中,然后输出Set中元素个数。


上面的例子我特意写的稍微复杂了一些,不过我几乎都加了注释。这里面涉及到了线程池的创建CountDownLatchlambda表达式、线程安全的HashSet等知识。感兴趣的朋友可以逐一了解一下。


正常情况下,以上代码输出结果应该是100。但是实际执行结果是一个小于100的数字。


原因就是因为`SimpleDateFormat`作为一个非线程安全的类,被当做了共享变量在多个线程中进行使用,这就出现了线程安全问题。

在阿里巴巴Java开发手册的第一章第六节一并发处理中关于这一点也有明确说明 :


在这里插入图片描述

那么,接下来我们就来看下到底是为什么,以及该如何解决。


✔️线程不安全原因


通过以上代码,我们发现了在并发场景中使用SimpleDateFormat会有线程安全问题。其实,JDK文档中已经明确表明了SimpleDateFormat不应该用在多线程场景中 :


Date formats are not synchronized. lt is recommended to create separate format instancesfor each thread. lf multiple threads access a format concurrently, it must be synchronizedexternally.


那么接下来分析下为什么会出现这种问题,SimpleDateFormat底层到底是怎么实现的?


我们跟一下SimpleDateFormat类中format方法的实现其实就能发现端倪。


在这里插入图片描述

SimpleDateFormat中的format方法在执行过程中,会使用一个成员变量calendar来保存时间。这其实就是问题的关键。


由于我们在声明SimpleDateFormat的时候,使用的是static定义的。那么这个SimpleDateFormat就是一个共享变量,随之,SimpleDateFormat中的calendar也就可以被多个线程访问到。


假设线程1刚刚执行完 calendar.setTime 把时间设置成2018-11-11,还没等执行完,线程2又执行了 calendar.setTime 把时间改成了2018-12-12。这时候线程1继续往下执行,拿到的 calendar.getTime 得到的时间就是线程2改过之后的。


除了format方法以外,SimpleDateFormat的parse方法也有同样的问题。


所以,不要把SimpleDateFormat作为一个共享变量使用。


✔️如何解决


前面介绍过了SimpleDateFormat存在的问题以及问题存在的原因,那么有什么办法解决这种问题呢?


解决方法有很多,这里介绍三个比较常用的方法。


✔️使用局部变量


for (int i = 0; i < 100; i++) {
	//获取当前时间
	Calendar calendar = Calendar.getInstance();
	int finalI = i;
	pool.execute(() -> {
		// SimpleDateFormat声明成局部变量
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		//时间增加
		calendar .add(Calendar .DATE, finalI);
		//通过simpleDateFormat把时间转换成字符串
		String dateString = simpleDateFormat.format(calendar.getTime());
		//把字符串放入Set中
		dates .add(datestring);
		//countDown
		countDownLatch .countDown();
	});
}

SimpleDateFormat变成了局部变量,就不会被多个线程同时访问到,就避免了线程安全问题。


✔️加同步锁


除了改成局部变量以外,还有一种方法大家可能比较熟悉的,就是对于共享变量进行枷锁。


for (int i = 0 ; i < 100; i++) {
	//获取当前时间
	Calendar calendar = Calendar.getInstance();
	int finalI = i;
	pool.execute(() -> {
		//加锁
		synchronized (simpleDateFormat)  {
			//时间增加
			calendar.add(Calendar .DATE, finalI);
			//通过simpleDateFormat把时间转换成字符串
			String dateString = simpleDateFormat.format(calendar.getTime());
			//把字符串放入Set中
			dates .add(datestring);
			//	countDown
			countDownLatch.countDown();
		}
	});
}

通过加锁,使多个线程排队顺序执行。避免了并发导致的线程安全问题。


其实以上代码还有可以改进的地方,就是可以把锁的粒度再设置的小一点,可以只对 simpleDateFormat.format 这一行加锁,这样效率更高一些。


✔️使用ThreadLocal


第=种方式,就是使用 ThreadLocalThreadLocal 可以确保每人线程都可以得到单独的一个SimpleDateFormat 的对象,那么自然也就不存在竞争问题了。


/**
*    使用ThreadLocal定义一个全局的simpleDateFormat
*/

private static Threadlocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
	@Override
	protected SimpleDateFormat initialValue() {
		return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	}
}

//用法
String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime());

ThreadLocal 来实现其实是有点类似于缓存的思路,每个线程都有一个独享的对象,避免了频繁创建对象,也避免了多线程的竞争。


当然,以上代码也有改进空间,就是,其实SimpleDateFormat的创建过程可以改为延迟加载。这里就不详细介绍了。


✔️使用DateTimeFormatter


如果是Java8应用,可以使用DateTimeFormatter代替SimpleDateFormat,这是一个线程安全的格式化工具类。就像官方文档中说的,这类 simple beautiful strong immutable thread-safe


//解析日期
string datestr="2016年10月25日";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy手M月dd:");
LocalDate date= LocalDate.parse(dateStr, formatter);

//日期转换为字符串
localDateTime now = LocalDateTime .now();
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
String nowStr = now.format(format);
System.outprintIn(nowStr);

2023年的最后一天,一篇SimpleDateFormat相关的详解带给大家,希望对大家有所帮助。


祝所有读者在接下来的2024年能一路长虹、心想事成、快乐天天相伴~,预祝大家新年快乐!

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

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

相关文章

基于YOLOv5+Deepsort 的PCB缺陷检测及计数系统

背景&#xff1a; PCB&#xff08;Printed Circuit Board&#xff0c;印刷电路板&#xff09;是电子产品中至关重要的组成部分&#xff0c;它承载着电子元器件并提供电气连接。在PCB制造过程中&#xff0c;由于工艺、材料或设备等因素的影响&#xff0c;可能会引入各种缺陷&am…

Seata AT TM->RC->RM一次完整的交互过程

原理 TM两阶段&#xff1a; 阶段1&#xff1a;TM向TC申请全局事务&#xff0c;netty客户端发起了一次记录xid的请求 阶段2&#xff1a;TC协调之后&#xff0c;决定执行RM是否提交或者回滚。 spring公共组件部分 1、SeataAutoConfiguration类 利用springboot自动装配机制从…

Java版商城:Spring Cloud+SpringBoot b2b2c电子商务平台,多商家入驻、直播带货及免 费 小程序商城搭建

随着互联网的快速发展&#xff0c;越来越多的企业开始注重数字化转型&#xff0c;以提升自身的竞争力和运营效率。在这个背景下&#xff0c;鸿鹄云商SAAS云产品应运而生&#xff0c;为企业提供了一种简单、高效、安全的数字化解决方案。 鸿鹄云商SAAS云产品是一种基于云计算的软…

【算法练习】leetcode链表算法题合集

链表总结 增加表头元素倒数节点&#xff0c;使用快慢指针环形链表&#xff08;快慢指针&#xff09;合并有序链表&#xff0c;归并排序LRU缓存 算法题 删除链表元素 删除链表中的节点 LeetCode237. 删除链表中的节点 复制后一个节点的值&#xff0c;删除后面的节点&#x…

多环境及SpringBoot项目部署

1、多环境 2、项目部署上线 原始前端 / 后端项目宝塔Linux容器容器平台 3、前后端联调 4、项目扩展和规划 多环境 程序员鱼皮-参考文章 本地开发&#xff1a;localhost&#xff08;127.0.0.1&#xff09; 多环境&#xff1a;指同一套项目代码在把不同的阶段需要根据实际…

【Vue2+3入门到实战】(17)VUE之VueCli脚手架自定认创建项目、ESlint代码规范与修复、 ESlint自动修正插件的使用 详细示例

目录 一、本节内容二、VueCli 自定义创建项目三、ESlint代码规范及手动修复1.JavaScript Standard Style 规范说明2.代码规范错误3.手动修正 四、通过eslint插件来实现自动修正 一、本节内容 VueCli脚手架自定认创建项目ESlint代码规范与修复ESlint自动修正插件 二、VueCli 自…

设备健康管理系统助力制造企业实现数字化转型

在当今快速变革的制造业环境中&#xff0c;数字化转型已成为制造企业保持竞争力和实现可持续发展的关键。在这个数字化转型的浪潮中&#xff0c;设备健康管理系统正发挥着重要的作用。设备健康管理系统通过实时监测、预测分析和智能诊断等功能&#xff0c;为制造企业提供了全面…

中医电子处方系统,西医个体诊所门诊卫生室病历记录查询软件教程

中医电子处方系统&#xff0c;西医个体诊所门诊卫生室病历记录查询软件教程 一、软件程序问答 1、电子处方软件如何快速开单&#xff1f; 如下图&#xff0c;软件以 佳易王诊所电子处方管理系统V17.1版本为例说明 在开电子处方的时候可以按单个药品开&#xff0c;也可以直…

动态规划10-多重背包

题目描述 有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用&#xff0c;每件耗费的空间是Ci &#xff0c;价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量&#xff0c;且价值总和最大。 思路分析 区别于完全背包和简单的01背包问题&…

数据的确权、流通、入表与监管研究(二)数据与流通

附&#xff1a;2023年数据资源入表白皮书下载&#xff1a; 关注WX公众号&#xff1a; commindtech77&#xff0c; 获得数据资产相关白皮书下载地址 1. 回复关键字&#xff1a;数据资源入表白皮书 下载 《2023数据资源入表白皮书》 2. 回复关键字&#xff1a;光大银行 下载 光…

牛客网面试题知识点记录-02

1.collection接口 2.在构造方法中调用方法A,若方法A被子类重写&#xff0c;则会先调用子类的方法A。举例如下题&#xff1a; 此时会输出null&#xff0c;调用顺序为&#xff1a;子类初始化&#xff0c;但是子类实现了Base&#xff0c;会先执行Base的构造方法&#xff0c;构造…

golang第五卷---包以及常用内置包归纳

包以及常用内置包归纳 包的概念math包time包sync包 Go 语言官方的包文档网站&#xff1a;包文档 包的概念 Go语言是使用包来组织源代码的&#xff0c;包&#xff08;package&#xff09;是多个 Go 源码的集合&#xff0c;是一种高级的代码复用方案。 任何源代码文件必须属于某…

2022–2023学年2021级计算机科学与技术专业数据库原理 (A)卷

一、单项选择题&#xff08;每小题1.5分&#xff0c;共30分&#xff09; 1、构成E—R模型的三个基本要素是&#xff08; B &#xff09;。 A&#xff0e;实体、属性值、关系 B&#xff0e;实体、属性、联系 C&#xff0e;实体、实体集、联系 D&#xff0e;实体、实体…

基于 CefSharp 实现一个文件小工具

I’m not saying you can’t be financially successful I’m saying have a greater purpose in life well beyond the pursuit of financial success Your soul is screaming for you to answer your true calling You can change today if you redefine what success is to …

分布式存储考点梳理 + 高频面试题

欢迎来到分布式存储模环节&#xff0c;本文我将和你一起梳理面试中分布式系统的数据库的高频考点&#xff0c;做到温故知新。 面试中如何考察分布式存储 广义的分布式存储根据不同的应用领域&#xff0c;划分为以下的类别&#xff1a; 分布式协同系统 分布式文件系统 分布式…

Android长按图标展示快捷方式

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {new Thread(() -> {// 获取ShortcutManager实例ShortcutManager shortcutManager getSystemService(ShortcutManager.class);// 创建要添加的快捷方式ShortcutInfo.Builder shortcutBuilder new ShortcutInfo.Bui…

OpenCV实战 -- 维生素药片的检测记数

文章目录 检测记数原图经过操作开始进行消除粘连性--形态学变换总结实现方法1. 读取图片&#xff1a;2. 形态学处理&#xff1a;3. 二值化&#xff1a;4. 提取轮廓&#xff1a;5. 轮廓筛选和计数&#xff1a; 分水岭算法&#xff1a;逐行解释在基于距离变换的分水岭算法中&…

dll不能运行是什么意思,分享5种有效的修复方法

在计算机使用过程中&#xff0c;我们可能会遇到各种各样的问题&#xff0c;其中一种常见的问题是“dll不能运行”。这个问题可能会影响到我们的正常使用&#xff0c;甚至导致某些软件无法启动。那么&#xff0c;“dll不能运行是什么意思”呢&#xff1f;dll文件丢失怎么恢复&am…

【数据结构】排序之交换排序(冒泡 | 快排)

交换目录 1. 前言2. 交换排序3. 冒泡排序3.1 分析3.2 代码实现 4. 快速排序4.1 hoare版本4.1.1 分析4.1.2 hoare版本代码 4.2 挖坑法4.2.1 分析4.2.2 挖坑法代码实现 4.3 前后指针版本4.3.1 分析4.3.2 前后指针版本代码实现 1. 前言 在之前的博客中介绍了插入排序&#xff0c;…

2023-12-20 LeetCode每日一题(判别首字母缩略词)

2023-12-20每日一题 一、题目编号 2828. 判别首字母缩略词二、题目链接 点击跳转到题目位置 三、题目描述 给你一个字符串数组 words 和一个字符串 s &#xff0c;请你判断 s 是不是 words 的 首字母缩略词 。 如果可以按顺序串联 words 中每个字符串的第一个字符形成字符…