SimpleDateFormat是Java中非常常见的一个类,用来解析和格式化日期字符串。但是SimpleDateFormat在多线程的环境并不是安全的,这个是很容易犯错的部分,接下来讲一下这个问题出现的过程以及解决的思路。

问题描述:
先看代码,用来获取一个月的天数的:

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date; public class DateUtil { /**
* 获取月份天数
* @param time 201202
* @return
*/
public static int getDays(String time) throws Exception {
// String time = "201202";
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
Date date = sdf.parse(time);
Calendar c = Calendar.getInstance();
c.setTime(date);
int day = c.getActualMaximum(Calendar.DATE);
return day;
} }

可以看到在这个方法里,每次要获取值的时候就先要创建一个SimpleDateFormat的实例,频繁调用这个方法的情况下很耗性能。为了避免大量实例的频繁创建和销毁,我们通常会使用单例模式或者静态变量进行改造,一般会这么改:

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date; public class DateUtil { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM"); /**
* 获取月份天数
* @param time 201202
* @return
*/
public static int getDays(String time) throws Exception {
// String time = "201202";
Date date = sdf.parse(time);
Calendar c = Calendar.getInstance();
c.setTime(date);
int day = c.getActualMaximum(Calendar.DATE);
return day;
} }

此时不管调用多少次这个方法,java虚拟机里只有一个SimpleDateFormat对象,效率和性能肯定要比第一个方法好,这个也是很多程序员选择的方法。但是,在这个多线程的条件下,多个thread共享同一个SimpleDateFormat,而SimpleDateFormat本身又是线程非安全的,这样就很容易出各种问题。

验证问题:
用一个简单的例子验证一下多线程环境下SimpleDateFormat的运行结果:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch; public class DateUtil {
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String format(Date date) {
return dateFormat.format(date);
} public static Date parse(String dateStr) throws ParseException {
return dateFormat.parse(dateStr);
} public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(1);
final String[] strs = new String[] {"2016-01-01 10:24:00", "2016-01-02 20:48:00", "2016-01-11 12:24:00"};
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
} for (int i = 0; i < 10; i++){
try {
System.out.println(Thread.currentThread().getName()+ "\t" + parse(strs[i % strs.length]));
Thread.sleep(100);
} catch (ParseException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
latch.countDown();
}
}

看一下运行的结果:

Thread-9    Fri Jan 01 10:24:00 CST 2016
Thread-1 Sat Feb 25 00:48:00 CST 20162017
Thread-5 Sat Feb 25 00:48:00 CST 20162017
Exception in thread "Thread-4" Exception in thread "Thread-6" java.lang.NumberFormatException: For input string: "2002.E20022E"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at DateUtil.parse(DateUtil.java:24)
at DateUtil$2.run(DateUtil.java:45)

那么为什么SimpleDateFormat不是线程安全的呢?

查找问题:

首先看一下SimpleDateFormat的源码:

private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date); boolean useDateFormatSymbols = useDateFormatSymbols(); for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
} switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break; case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break; default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}

可以看到format()方法先将日期存放到一个Calendar对象中,而这个Calendar在SimpleDateFormat中是以成员变量的形式存在的。随后调用subFormat()时会再次用到成员变量Calendar。这就是问题所在。同样,在parse()方法里也会存在相应的问题。
试想,在多线程环境下,如果两个线程都使用同一个SimpleDateFormat实例,那么就有可能存在其中一个线程修改了calendar后紧接着另一个线程也修改了calendar,那么随后第一个线程用到calendar时已经不是它所期待的值了。

避免问题:

那么,如何保证SimpleDateFormat的线程安全呢?
1.每次使用SimpleDateFormat时都创建一个局部的SimpleDateFormat对象,跟一开始的那个方法一样,但是存在性能上的问题,开销较大。
2.加锁或者同步

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date; public class DateSyncUtil { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String formatDate(Date date)throws ParseException{
synchronized(sdf){
return sdf.format(date);
}
} public static Date parse(String strDate) throws ParseException{
synchronized(sdf){
return sdf.parse(strDate);
}
}
}

当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。
3.使用ThreadLocal

public class DateUtil {
private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
}; public static String format(Date date) {
return local.get().format(date);
} public static Date parse(String dateStr) throws ParseException {
return local.get().parse(dateStr);
}
}

使用ThreadLocal可以确保每个线程都可以得到一个单独的SimpleDateFormat对象,既避免了频繁创建对象,也避免了多线程的竞争。

SimpleDateFormat,Calendar 线程非安全的问题的更多相关文章

  1. JDK中的SimpleDateFormat线程非安全

    在JDK中使用SimpleDateFormat的时候都会遇到线程安全的问题,在JDK文档中也说明了该类是线程非安全的,建议对于每个线程都创建一个SimpleDateFormat对象.如下面一个Case ...

  2. 如何在一个线程环境中使用一个线程非安全的java类

    在开发过程中 当我们拿到一个线程非安全的java类的时候,我们可以额外创建这个类的管理类 并在管理类中控制同步 比如 一个非线程安全的Pair类 package test.thread.sx.test ...

  3. 06_java 时间获取练习_Date\SimpleDateFormat\Calendar类练习

     1.获取当前的日期,并把这个日期转换为指定格式的字符串,如2088-08-08 08:08:08 import java.text.SimpleDateFormat; import java.uti ...

  4. 时间获取_Date\SimpleDateFormat\Calendar类

     1.获取当前的日期,并把这个日期转换为指定格式的字符串,如2088-08-08 08:08:08 import java.text.SimpleDateFormat; import java.uti ...

  5. java基础1.5版后新特性 自动装箱拆箱 Date SimpleDateFormat Calendar.getInstance()获得一个日历对象 抽象不要生成对象 get set add System.arrayCopy()用于集合等的扩容

    8种基本数据类型的8种包装类 byte Byte short Short int Integer long Long float Float double Double char Character ...

  6. Java基础 与时间日期相关的类:System -Date -SimpleDateFormat -Calendar类 -解决后缀.000Z 的时区问题

    笔记总结: /**与时间相关的类:System_Date_SimpleDateFormat_Calendar类 * 1.system 类下的currentTimeMillis() * 输出从1970年 ...

  7. session/SessionFactory线程非安全和线程安全

    SessionFactory负责创建session,SessionFactory是线程安全的,多个并发线程可以同时访问一个 SessionFactory 并从中获取Session实例. (Sessio ...

  8. ios专题 - 多线程非GCD(1)

    iOS多线程初体验是本文要介绍的内容,iPhone中的线程应用并不是无节制的,官方给出的资料显示iPhone OS下的主线程的堆栈大小是1M,第二个线程开始都是512KB.并且该值不能通过编译器开关或 ...

  9. 【源码】HashMap源码及线程非安全分析

    最近工作不是太忙,准备再读读一些源码,想来想去,还是先从JDK的源码读起吧,毕竟很久不去读了,很多东西都生疏了.当然,还是先从炙手可热的HashMap,每次读都会有一些收获.当然,JDK8对HashM ...

随机推荐

  1. 给trac的ticket添加提交时字段验证

    我们在项目管理中使用了trac系统,并且对于ticket添加了以下自定义字段并且对它们的格式都有一定要求: svn版本号:格式为 r1234.多个版本号之间使用半角逗号隔开.如:r1234,r5678 ...

  2. php数组的定义和数组的赋值

    1.php执行过程 加载页面  语法检测 执行脚本 $arr=array(1,2,3); 索引数组 $arr=array("name"=>"user1", ...

  3. LayIM.AspNetCore Middleware 开发日记(四)主角登场(LayIM介绍)

    前言 在前几篇中已经初步介绍了开发AspNetCore中间件的一些基础知识,不过都没有很深入的去研究,后续还是需要去看看源码.本篇呢,终于有点开头的味道了,就是要介绍LayIM了,其实标题写的是主角, ...

  4. 新闻cms管理系统(三) ------菜单管理

    1.前期准备工作 (1)模板介绍 添加菜单的模板页面 菜单管理首页: 添加菜单页面: (2)公共类引入介绍 公共函数文件的引入(位置: Application/Admin/Controller/Com ...

  5. 13.56Mhz SI522兼容MFRC522的资料以及对比性能

    (13.56Mhz芯片) SI522是一颗专门替代MFRC522/FM17522,PIN对PIN 完全软硬件兼容.相对于MFRC522,SI522完全替换,不需要做任何更改,同时接受模式下功耗低10m ...

  6. python -- peewee处理数据库连接

    目前,实现了的Database子类有三个:SqliteDatabase.MySQLDatabase.PostgresqlDatabase class SqliteDatabase(Database) ...

  7. java通过IO流复制文件

    package kimoji; import java.io.*; public class FileTest { public static void main(String[] args) thr ...

  8. 【css】table标签内的td、th如何设置固定宽度,而不是自适应?

    table{ min-width: %; } td{ min-width: 100px; } .table-container{ overflow:auto; display: block; } &l ...

  9. code#5 P1 报告

    报告   时间限制: 1.0 秒 空间限制: 128 MB 相关文件: 题目目录 题目描述 企鹅高中有很多学生,自然管理起来也就非常麻烦.学校的教务处想要随时统计学校里面有多少个学生,但是他们只有很多 ...

  10. Ubuntu下配置MySql

    安装mysql sudo apt-get install mysql-server 检查安装是否成功 sudo netstat -tap | grep mysql 登录mysql mysql -uro ...