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

news2025/1/23 9:26:58

面试回答

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

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

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

  1. 【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static ,必须家锁,或者使用 DateUitls 工具类。

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

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

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

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

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

知识扩展

SimpleDateFormat 用法

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

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

// Date 转 String
Date date=new Date();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateSr=sdf.format(date);
System.out.println(dateSr);

以上代码,转换的结果是:2023-07-13 14:36:20,日期和时间格式由“日期和时间格式”字符串指定。如果你想要转换成其他格式,只要指定不同的时间模式就行了。

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

//String 转 Date
System.out.println(sdf.parse(dateSr));

日期和时间模式表达方式

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

符号

含义

示例

G

年代/时代

AD

y

年份(四位数)

2019

M

月份

7, 07

d

月份中的天数

10

h

时(12小时制)

3

H

时(24小时制)

15

m

分钟

30

s

55

S

毫秒

234

E

星期几

Tue, Tuesday

D

年中的天数

189

F

月份中的星期几

2

w

年份中的周数

27

W

月份中的周数

2

a

上午/下午标记

AM, PM

k

时(24小时制,无前导零)

3

K

时(12小时制,无前导零)

3

z

时区

GMT+08:00

Z

RFC 822时区偏移量

+0800, -0800

X

ISO 8601时区

+08, -0800, Z

输出不同时区的时间

时区是地球上的区域使用同一个时间定义。以前,人们通过观察太阳的位置(时角)决定时间,这就使得不同精度的地方的时间有所不同(地方时)。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()));

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

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

SimpleDateFormat 线程安全性

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

public class Main {

    private static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    public static void main(String[] args) {
        sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
        System.out.println(sdf.format(Calendar.getInstance().getTime()));
    }
    
}

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

问题重现

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


    // 定义一个全局的 SimpleDateFormat
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

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

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingDeque<Runnable>(1024),
            threadFactory,
            new ThreadPoolExecutor.AbortPolicy());

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


    public static void main(String[] args) throws InterruptedException {
        // 定义一个线程安全的 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=sdf.format(calendar.getTime());
                // 把字符串放入 Set 中
                dates.add(dateString);
                // countDownLatch
                countDownLatch.countDown();
            });
        }
        // 阻塞,直到 countDownLatch 数量为 0
        countDownLatch.await();
        System.out.println(dates.size());
    }

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

上面的例子我特意写的稍微复杂了一些不过我几乎都加了注释。这里面设计到了线程池的创建、CountDownLatch、lambda 表达式、线程安全的 HashSet 等知识。刚性取得朋友可以逐一了解一下。

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

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

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

  1. 【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static ,必须家锁,或者使用 DateUitls 工具类。

正例:注意线程安全,使用 DateUitls。亦推荐如下处理:

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

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

线程不安全的原因

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

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

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

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

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

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

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

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

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

如何解决

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

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

使用局部变量

    public static void main(String[] args) throws InterruptedException {
        // 定义一个线程安全的 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 声明成局部变量
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                // 通过 SimpleDateFormat 把时间转换成字符串
                String dateString=sdf.format(calendar.getTime());
                // 把字符串放入 Set 中
                dates.add(dateString);
                // countDownLatch
                countDownLatch.countDown();
            });
        }
        // 阻塞,直到 countDownLatch 数量为 0
        countDownLatch.await();
        System.out.println(dates.size());
    }

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

加同步锁

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

    public static void main(String[] args) throws InterruptedException {
        // 定义一个线程安全的 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(()->{
                synchronized (sdf){
                    // 时间增加
                    calendar.add(Calendar.DATE,finalI);
                    // 通过 SimpleDateFormat 把时间转换成字符串
                    String dateString=sdf.format(calendar.getTime());
                    // 把字符串放入 Set 中
                    dates.add(dateString);
                    // countDownLatch
                    countDownLatch.countDown();
                }
            });
        }
        // 阻塞,直到 countDownLatch 数量为 0
        countDownLatch.await();
        System.out.println(dates.size());
    }

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

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

使用 ThreadLocal

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

public class Main {

    // 使用 ThreadLocal 定义一个全局的 SimpleDateFormat
    private static final ThreadLocal<DateFormat> df= ThreadLocal.withInitial(()
            -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    // 使用 ThreadFactory 定义一个线程池
    private static ThreadFactory threadFactory = new ThreadFactoryBuilder()
            .setNameFormat("demo-pool-%d")
            .build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingDeque<Runnable>(1024),
            threadFactory,
            new ThreadPoolExecutor.AbortPolicy());

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


    public static void main(String[] args) throws InterruptedException {
        // 定义一个线程安全的 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=df.get().format(calendar.getTime());
                // 把字符串放入 Set 中
                dates.add(dateString);
                // countDownLatch
                countDownLatch.countDown();
            });
        }
        // 阻塞,直到 countDownLatch 数量为 0
        countDownLatch.await();
        System.out.println(dates.size());
    }
}

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

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

使用 DateTimeFormatter

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

    public static void main(String[] args) {
        // 解析日期
        String dateStr = "2023-07-12 23:00:00";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime date = LocalDateTime.parse(dateStr, formatter);
        System.out.println(date);

        //日期转换为字符串
        LocalDateTime now=LocalDateTime.now();
        String nowStr=now.format(formatter);
        System.out.println(nowStr);
    }

 

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

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

相关文章

leetcode 718. 最长重复子数组

2023.8.24 本题求得子数组&#xff0c;其实就是连续的序列。定义一个二维dp数组&#xff0c;dp[i][j]的含义为&#xff1a;以下标i为结尾的nums1和以下标j为结尾的nums2之间的公共最长子数组。 易得&#xff1a;递推公式为dp[i][j] dp[i-1][j-1] 1&#xff1b; 由此可以看出当…

电商项目part05 微服务网关整合OAuth2.0授权中心

微服务网关整合 OAuth2.0 思路分析 网关整合 OAuth2.0 有两种思路&#xff0c;一种是授权服务器生成令牌, 所有请求统一在网关层验证&#xff0c;判断权 限等操作&#xff1b;另一种是由各资源服务处理&#xff0c;网关只做请求转发。 比较常用的是第一种&#xff0c;把API网关…

从零做软件开发项目系列之四——数据库设计

前言 在对软件进行设计的过程中&#xff0c;数据库的设计是一项重要的内容&#xff0c;软件中主要的处理对象就是各类业务数据&#xff0c;通过对业务数据的处理&#xff0c;实现各种功能。我们经常说的&#xff0c;写程序&#xff0c;说到底就是增删改查&#xff0c;而增删改…

IDEA远程开发

IDEA远程开发 前期准备 IDEA的远程开发是在本地去操昨远程服务器上的代码&#xff0c;所以我们先需要准备一台服务器,在此我使用vmware虚拟出ubuntu-20.04.6的Server版本,以便后面演示。 Ubuntu的Java环境配置 JDK8 sudo apt install openjdk-8-jdkmaven sudo apt instal…

NAT的配置实验

一、实验目的 学习如何配置NAT 二、预备知识: Net Address Translation:通过将内部用户的地址转换为1个公用的外部地址&#xff0c;然后再与外部的用户进行通信&#xff0c;从而既节省了IPv4地址&#xff0c;又实现了对内部用户的安全保护 三、实验过程&#xff1a; …

unity动画融合

1、抛砖引玉 在大型复杂的场景中&#xff0c;一定遇到过手在鼓掌&#xff0c;头在摇头&#xff0c;腿又是其他动作的要求&#xff0c;但是这些东西又不能做一起&#xff0c;因为有时候要把某个动画单独使用&#xff0c;这时候就用到了动画融合&#xff0c;利用动画状态机分层机…

taro react/vue h5 中的上传input onchange 值得区别

<inputclassNamebase-input-file-h5typefileacceptimage/*capturecameraonChange{onChangeInput} />1、taro3react 2、taro3vue3

Android12之ABuffer数据处理(三十四)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:Android…

【1267. 统计参与通信的服务器】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 这里有一幅服务器分布图&#xff0c;服务器的位置标识在 m * n 的整数矩阵网格 grid 中&#xff0c;1 表示单元格上有服务器&#xff0c;0 表示没有。 如果两台服务器位于同一行或者同一列&#xff…

docker可视化工具

安装Portainer 官方安装说明&#xff1a;https://www.portainer.io/installation/ [rootubuntu1804 ~]#docker pull portainer/portainer[rootubuntu1804 ~]#docker volume create portainer_data portainer_data [rootubuntu1804 ~]#docker run -d -p 8000:8000 -p 9000:90…

深入理解 Vue Router:构建可靠的前端路由系统

目录 01-什么是前端路由以及路由两种模式实现原理02-路由的基本搭建与嵌套路由模式03-动态路由模式与编程式路由模式04-命名路由与命名视图与路由元信息05-路由传递参数的多种方式及应用场景06-详解route对象与router对象07-路由守卫详解及应用场景 01-什么是前端路由以及路由两…

XShell 使用命令登陆主机

以root&#xff08;管理者&#xff09;身份登录主机 假设&#xff1a; root的ip&#xff1a;111.00.111.000 root的密码&#xff1a;123456 命令格式&#xff1a; ssh root(这里填ip) //接着回车&#xff0c;输入密码即可实操&#xff1a; enter回车后&#xff0c;在弹出的窗口…

基于Roop视频换脸

Roop 是一个强大的一键换脸工具&#xff0c;允许用户在视频中替换面部&#xff0c;只需要目标面部的一张图片&#xff0c;无需数据集&#xff0c;无需训练。 相对于之前的 Simswap 来说效果要好很多&#xff0c;不过需要注意的是没有授权不要商用。 文章目录 环境搭建使用方法…

使用Hydra进行密码暴力破解

Hydra是一款强大的密码暴力破解工具&#xff0c;可用于尝试使用不同的用户名和密码组合来破解各种登录系统&#xff0c;如SSH、FTP、HTTP等。 步骤&#xff1a; 选择目标&#xff1a; 首先&#xff0c;选择 要尝试破解的目标系统&#xff0c;例如SSH服务器、FTP服务器或Web应用…

Python用 tslearn 进行时间序列聚类可视化

全文链接&#xff1a;https://tecdat.cn/?p33484 我们最近在完成一些时间序列聚类任务&#xff0c;偶然发现了 tslearn 库。我很想看看启动和运行 tslearn 已内置的聚类有多简单&#xff0c;结果发现非常简单直接&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09…

python基础—python6种基本数据类型及数据类型之间转换

文章目录 一、python标准数据类型&#xff08;一&#xff09;数字类型整型&#xff1a;int浮点型&#xff1a;flaot布尔型&#xff1a;bool复数类型&#xff1a;complex &#xff08;二&#xff09;字符串&#xff08;三&#xff09;列表类型&#xff08;四&#xff09;元组类型…

cvc-complex-type.2.4.a: 发现了以元素 ‘base-extension‘ 开头的无效内容。应以 ‘{layoutlib}‘ 之一开头

不能飞的猪只是没用的猪。 —— 宫崎骏 《红猪》 常见的1种case 记录一下&#xff0c;新电脑安装android studio导入公司那些gradle还是5.5左右的工程以后&#xff0c;各种不适应。编译问题出现了。老电脑都是好好的。 cvc-complex-type.2.4.a: 发现了以元素 ‘base-extensi…

jvm的内存划分区域

jvm划分5个区域&#xff1a; java虚拟机栈、本地方法栈、堆、程序计数器、方法区。 各个区各自的作用&#xff1a; 1.本地方法栈&#xff1a;用于管理本地方法的调用&#xff0c;里面并没有我们写的代码逻辑&#xff0c;其由native修饰&#xff0c;由 C 语言实现。 2.程序计数…

生成式人工智能的潜在有害影响与未来之路(三)

产品责任法的潜在适用 背景和风险 产品责任是整个二十世纪发展起来的一个法律领域&#xff0c;旨在应对大规模生产的产品可能对社会造成的伤害。这一法律领域侧重于三个主要危害&#xff1a;设计缺陷的产品、制造缺陷的产品和营销缺陷的产品。产品责任法的特点有两个要素&…

PHPEXCEL 导出excel

$styleArray [alignment > [horizontal > Alignment::HORIZONTAL_CENTER,vertical > Alignment::VERTICAL_CENTER],];$border_style [borders > [allborders > [style > \PHPExcel_Style_Border::BORDER_THIN ,//细边框]]];$begin_date $request->beg…