线程不安全的SimpleDateFormat

8.5 SimpleDateFormat是线程不安全的

SimpleDateFormat是Java提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个SimpleDateFormat实例对日期进行解析或者格式化会导致程序出错,本节就讨论下它为何是线程不安全的,以及如何避免。

问题复现

为了复现该问题,编写如下代码:

 public class TestSimpleDateFormat {
    //(1)创建单例实例
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        //(2)创建多个线程,并启动
        for (int i = 0; i <10 ; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {//(3)使用单例日期实例解析文本
                        System.out.println(sdf.parse("2017-12-13 15:17:27"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();//(4)启动线程
        }
    }
}

代码(1)创建了SimpleDateFormat的一个实例,代码(2)创建10个线程,每个线程都公用同一个sdf对象对文本日期进行解析,多运行几次就会抛出java.lang.NumberFormatException异常,加大线程的个数有利于该问题复现。

问题分析

为了便于分析首先奉上SimpleDateFormat的类图结构:

image.png

可知每个SimpleDateFormat实例里面有一个Calendar对象,从后面会知道其实SimpleDateFormat之所以是线程不安全的就是因为Calendar是线程不安全的,后者之所以是线程不安全的是因为其中存放日期数据的变量都是线程不安全的,比如里面的fields,time等。

下面从代码层面看下parse方法做了什么事情:

    public Date parse(String text, ParsePosition pos)
    {
       
        //(1)解析日期字符串放入CalendarBuilder的实例calb中
        .....

        Date parsedDate;
        try {//(2)使用calb中解析好的日期数据设置calendar
            parsedDate = calb.establish(calendar).getTime();
            ...
        }
       
        catch (IllegalArgumentException e) {
           ...
            return null;
        }

        return parsedDate;
    }
Calendar establish(Calendar cal) {
   ...
   //(3)重置日期对象cal的属性值
   cal.clear();
   //(4) 使用calb中中属性设置cal
   ...
   //(5)返回设置好的cal对象
   return cal;
}
  • 代码(1)主要的作用是解析字符串日期并把解析好的数据放入了 CalendarBuilder的实例calb中,CalendarBuilder是一个建造者模式,用来存放后面需要的数据。
  • 代码(3)重置Calendar对象里面的属性值,如下代码:
    public final void clear()
   {
       for (int i = 0; i < fields.length; ) {
           stamp[i] = fields[i] = 0; // UNSET == 0
           isSet[i++] = false;
       }
       areAllFieldsSet = areFieldsSet = false;
       isTimeSet = false;
   }
  • 代码(4)使用calb中解析好的日期数据设置cal对象
  • 代码(5) 返回设置好的cal对象

从上面步骤可知步骤(3)(4)(5)操作不是原子性操作,当多个线程调用parse 方法时候比如线程A执行了步骤(3)(4)也就是设置好了cal对象,在执行步骤(5)前线程B执行了步骤(3)清空了cal对象,由于多个线程使用的是一个cal对象,所以线程A执行步骤(5)返回的就可能是被线程B清空后的对象,当然也有可能线程B执行了步骤(4)被线程B修改后的cal对象。从而导致程序错误。

那么怎么解决那?

  • 第一种方式:每次使用时候new一个SimpleDateFormat的实例,这样可以保证每个实例使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其它引用,就会需要被回收,开销会很大。
  • 第二种方式:究其原因是因为多线程下步骤(3)(4)(5)三个步骤不是一个原子性操作,那么容易想到的是对其进行同步,让(3)(4)(5)成为原子操作,可以使用synchronized进行同步,具体如下:
public class TestSimpleDateFormat {
    // (1)创建单例实例
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        // (2)创建多个线程,并启动
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {// (3)使用单例日期实例解析文本
                        synchronized (sdf) {
                            System.out.println(sdf.parse("2017-12-13 15:17:27"));
                        }
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();// (4)启动线程
        }
    }
}

使用同步意味着多个线程要竞争锁,在高并发场景下会导致系统响应性能下降。

  • 第三种方式:使用ThreadLocal,这样每个线程只需要使用一个SimpleDateFormat实例相比第一种方式大大节省了对象的创建销毁开销,并且不需要对多个线程直接进行同步,使用ThreadLocal方式代码如下:
public class TestSimpleDateFormat2 {
    // (1)创建threadlocal实例
    static ThreadLocal<DateFormat> safeSdf = new ThreadLocal<DateFormat>(){
        @Override 
        protected SimpleDateFormat initialValue(){
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    
    public static void main(String[] args) {
        // (2)创建多个线程,并启动
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {// (3)使用单例日期实例解析文本
                            System.out.println(safeSdf.get().parse("2017-12-13 15:17:27"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();// (4)启动线程
        }
    }
}

代码(1)创建了一个线程安全的SimpleDateFormat实例,步骤(3)在使用的时候首先使用get()方法获取当前线程下SimpleDateFormat的实例,在第一次调用ThreadLocal的get()方法适合会触发其initialValue方法用来创建当前线程所需要的SimpleDateFormat对象。

总结

本节通过简单介绍SimpleDateFormat的原理说明了SimpleDateFormat是线程不安全的,应该避免多线程下使用SimpleDateFormat的单个实例,多线程下使用时候最好使用ThreadLocal对象。更多并发编程中需要注意的情景以及解决方法敬请期待 Java中高并发编程必备基础之并发包源码剖析 一书出版

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Python学习心得

Python爬虫--- 1.1请求库的安装与使用

原文链接:https://www.fkomm.cn/article/2018/7/16/16.html

8700
来自专栏牛客网

百词斩Java一面面经

19110
来自专栏技术博文

bash脚本 while语法

基本语法(比较常见的两种形式): 只要特定条件为真,”while” 语句就会执行 while [ condition ] do command1 c...

27850
来自专栏我的博客

ThinkPHP3.1.2笔记

1.开启trace 方法一:在配置文件中添加(默认在config.php,如果定义debug模式,可以定义在debug.php) SHOW_PAGE_TRAC...

27280
来自专栏linjinhe的专栏

LevelDB:读操作

前面写了两篇文章介绍 LevelDB 的整体架构和接口使用。这篇文章,我们从代码的角度看看 LevelDB 的设计与实现,先从读操作开始。

41930
来自专栏小鄧子的技术博客专栏

简单线程池的实现

要使用上述线程池,还需要一个永不退出的工作现场与之配合。是一个While循环,手动关闭之前,永不结束,一直等待新的任务进来。

10210
来自专栏Java帮帮-微信公众号-技术文章全总结

JavaWeb15-DBUtils(Java真正的全栈开发)

? DBUtils 一.DBUtils 1. DButils的介绍 dbutils的介绍及其作用 DBUtils是java编程中的数据库操作实用工具,小巧简单...

24450
来自专栏码农阿宇

Asp.net Core 2.1新功能Generic Host(通用主机)深度学习

这是在Asp.Net Core 2.1加入了一种新的Host,现在2.1版本的Asp.Net Core中,有了两种可用的Host。

18120
来自专栏编舟记

Java高编译低运行错误(ConcurrentHashMap.keySet)

本地使用maven编译和运行时一切都正常,但是通过ci的方式,编译、打包、发布到部署环境,运行时抛出了一条显而易见的JDK版本的错误。

13730
来自专栏Python爬虫与算法进阶

Scrapy源码(2)——爬虫开始的地方

Scrapy运行命令 一般来说,运行Scrapy项目的写法有,(这里不考虑从脚本运行Scrapy) Usage examples: $ scrapy craw...

35430

扫码关注云+社区

领取腾讯云代金券