SimpleDateFormat 线程不安全及解决方案

Stella981
• 阅读 580

SimpleDateFormat定义

SimpleDateFormat 是一个以与语言环境有关的方式来格式化和解析日期的具体类。它允许进行格式化(日期 -> 文本)、解析(文本 -> 日期)和规范化。

SimpleDateFormat 使得可以选择任何用户定义的日期-时间格式的模式。但是,仍然建议通过 DateFormat 中的 getTimeInstance、getDateInstance 或 getDateTimeInstance 来创建日期-时间格式器。每一个这样的类方法都能够返回一个以默认格式模式初始化的日期/时间格式器。可以根据需要使用 applyPattern 方法来修改格式模式。

官网同步建议

同步
日期格式是不同步的。建议为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须是外部同步的。

为什么线程不安全

SimpleDateFormat 线程不安全及解决方案

上图中,SimpleDateFormat类中,有个对象calendar

**[calendar](http://tool.oschina.net/uploads/apidocs/jdk-zh/java/text/DateFormat.html#calendar)** 
          DateFormat 使用 calendar 来生成实现日期和时间格式化所需的时间字段值。

当SimpleDateFormat用static申明,多个线程共享SimpleDateFormat对象是,也共享该对象的calendar对象。而当调用parse方法时,会clear所有日历字段和值。当线程A正在调用parse,线程B调用clear,这样解析后的数据就会出现偏差

//parse方法
@Override
public Date parse(String text, ParsePosition pos)
{
    try {
            parsedDate = calb.establish(calendar).getTime();
            ...
    }
}

//establish方法
Calendar establish(Calendar cal) {
  ...  
  //将此 Calendar 的所有日历字段值和时间值(从历元至现在的毫秒偏移量)设置成未定义  
  cal.clear();  
}

同样 formart中也用到了calendar对象,将date设置到日历的时间字段中

private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);
        ...
}

当线程A调用setTime,而线程B也调用setTime,这时候线程A最后得到的时间是 最后线程B的时间。也会导致数据偏差

不安全示例:

public static void main(String[] args) throws InterruptedException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = "1111-11-11 11:11:11";
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for(int i=0;i<100;i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        //多个线程操作同一个sdf对象
                        System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName());
                    } catch (ParseException e) {
                        System.out.println("--------------> error, " + e.getMessage());
                    }
                }
            });

        }
        executorService.shutdown();
    }                                                    

执行结果:

...
1111-11-11 11:11:11---pool-1-thread-69
0011-11-11 11:11:11---pool-1-thread-72
0011-11-11 11:11:11---pool-1-thread-71
1111-11-11 11:11:11---pool-1-thread-73
1111-11-11 11:11:11---pool-1-thread-75
1111-11-11 11:11:11---pool-1-thread-76
1111-11-11 11:11:11---pool-1-thread-89
1111-11-11 00:11:11---pool-1-thread-93
1111-11-11 11:11:11---pool-1-thread-96
...

可以看到数据出现偏差

解决方案

1.为每个实例创建一个单独的SimpleDateFormat对象

public static void main(String[] args) throws InterruptedException {
        //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = "1111-11-11 11:11:11";
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for(int i=0;i<100;i++) {
            executorService.submit(new Runnable() {
                //为每个线程创建自己的sdf对象
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                @Override
                public void run() {
                    try {
                        System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName());
                    } catch (ParseException e) {
                        System.out.println("--------------> error, " + e.getMessage());
                    }
                }
            });

        }
        executorService.shutdown();
    }

  缺点:每次new一个实例,都会new一个format对象,虚拟机内存消耗大,垃圾回收频繁

2.给静态SimpleDateFormat对象加锁,使用Lock或者synchronized修饰

public static void main(String[] args) throws InterruptedException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = "1111-11-11 11:11:11";
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for(int i=0;i<100;i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                   //加同步锁
                    synchronized (sdf) {
                        try {
                            System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName());
                        } catch (ParseException e) {
                            System.out.println("--------------> error, " + e.getMessage());
                        }
                    }
                }
            });

        }
        executorService.shutdown();
    }

  缺点:性能差,其他线程要等待锁释放

3.使用ThreadLocal为每个线程创建一个SimpleDateFormat对象副本,有线程隔离性,各自的副本对象也不会被其他线程影响

public static void main(String[] args) throws InterruptedException {
        //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //初始化threadLocal并设置值
        ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            }
        };
        String dateStr = "1111-11-11 11:11:11";
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for(int i=0;i<100;i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(threadLocal.get().format(threadLocal.get().parse(dateStr)) + "---" + Thread.currentThread().getName());
                    } catch (ParseException e) {
                        System.out.println("--------------> error, " + e.getMessage());
                    }
                }
            });

        }
        executorService.shutdown();

        //清理threadLocal,生产环境不清理容易导致内存溢出        threadLocal.remove();

}

ThreadLocal原理分析

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Karen110 Karen110
2年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表(关于日期时间时分秒显示不出来)
在使用皕杰报表设计器时,数据据里面是日期型,但当你web预览时候,发现有日期时间类型的数据时分秒显示不出来,只有年月日能显示出来,时分秒显示为0:00:00。1.可以使用tochar解决,数据集用selecttochar(flowdate,"yyyyMMddHH:mm:ss")fromtablename2.也可以把数据库日期类型date改成timestamp
Wesley13 Wesley13
2年前
Java8为什么提供LocalDate、LocalTime、LocalDateTime 时间类
Java8为什么提供LocalDate、LocalTime、LocalDateTime时间类?Date不格式化打印可读性差。TueSep1009:34:04CST2019使用SimpleDateFormat对时间进行格式化,但SimpleDateFormat是线程不安全
Stella981 Stella981
2年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Stella981 Stella981
2年前
Python time模块 返回格式化时间
常用命令  strftimetime.strftime("%Y%m%d%H:%M:%S",formattime)第二个参数为可选参数,不填第二个参数则返回格式化后的当前时间日期201812112:00:00time.strftime('%H:%M:%S')返回当前时间的时分秒time.strftim
Wesley13 Wesley13
2年前
JAVA_常用类
//importjava.text.SimpleDateFormat;//格式化日期//importjava.util.Calendar;//日期//importjava.util.Date;//时间//importjava.util.GregorianCalendar;//Calendar的子类import
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
京东云开发者 京东云开发者
8个月前
SimpleDateFormat 线程安全问题修复方案 | 京东物流技术团队
在日常的开发过程中,我们不可避免地会使用到JDK8之前的Date类,在格式化日期或解析日期时就需要用到SimpleDateFormat类,但由于该类并不是线程安全的,所以我们常发现对该类的不恰当使用会导致日期解析异常,从而影响线上服务可用率。
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这