Java并发编程笔记之SimpleDateFormat源码分析
SimpleDateFormat 是 Java 提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个 SimpleDateFormat 实例对日期进行解析或者格式化会导致程序出错,本节就讨论下它为何是线程不安全的,以及如何避免。
为了复现上面所说的不安全,我们要用一个例子来突出这个不安全,例子如下:
package com.hjc; import java.text.ParseException;
import java.text.SimpleDateFormat; /**
* Created by cong on 2018/7/12.
*/
public class SimpleDateFormatTest { //(1)创建单例实例
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) {
//(2)创建多个线程,并启动
for (int i = ; i < ; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {//(3)使用单例日期实例解析文本
System.out.println(sdf.parse("2018-07-12 15:18:00"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
thread.start();//(4)启动线程
}
} }
运行结果如下:

代码(1)创建了 SimpleDateFormat 的一个实例,代码(2)创建 10 个线程,每个线程都公用同一个 sdf 对象对文本日期进行解析,多运行几次就会抛出 java.lang.NumberFormatException 异常,加大线程的个数有利于该问题复现。
为什么会出现这样的问题呢?
那么接下来我们就要进入到SimpleDateFormat 源码一探究竟,为了便于分析首先查看 SimpleDateFormat 的类图结构,类图如下所示:

可知每个 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 = ; i < fields.length; ) {
stamp[i] = fields[i] = ; // 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 对象。从而导致程序错误。
那么,让我们思考一个问题,如何解决SimpleDateFormat 的线程安全性问题呢?
1.第一种方式:每次使用时候 new 一个 SimpleDateFormat 的实例,这样可以保证每个实例使用自己的 Calendar 实例, 但是每次使用都需要 new 一个对象,并且使用后由于没有其它引用,就会需要被回收,开销会很大。
2.第二种方式:究其原因是因为多线程下代码(3)(4)(5)三个步骤不是一个原子性操作,那么容易想到的是对其进行同步,让(3)(4)(5)成为原子操作,可以使用 synchronized 进行同步,例子改造如下所示:
/**
* Created by cong on 2018/7/12.
*/
public class SimpleDateFormatTest1 { //(1)创建单例实例
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) {
//(2)创建多个线程,并启动
for (int i = ; i < ; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {// (3)使用单例日期实例解析文本
synchronized (sdf) {
System.out.println(sdf.parse("2018-07-12 15:18:00"));
}
} catch (ParseException e) {
e.printStackTrace();
}
}
});
thread.start();//(4)启动线程
}
} }
运行结果如下:

3.第三种方式:使用 ThreadLocal,这样每个线程只需要使用一个 SimpleDateFormat 实例相比第一种方式大大节省了对象的创建销毁开销,并且不需要对多个线程直接进行同步,使用 ThreadLocal 方式来保证线程安全,例子如下:
/**
* Created by cong on 2018/7/12.
*/
public class SimpleDateFormatTest2 {
// (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 = ; i < ; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {// (3)使用单例日期实例解析文本
System.out.println(safeSdf.get().parse("2018-07-12 15:18:00"));
} catch (ParseException e) {
e.printStackTrace();
}finally {
//(4)使用完毕记得清除,避免内存泄露
safeSdf.remove();
}
}
});
thread.start();// (4)启动线程
}
} }
运行结果如下:

代码(1)创建了一个线程安全的 SimpleDateFormat 实例,代码(3)在使用的时候首先使用 get() 方法获取当前线程下 SimpleDateFormat 的实例,在第一次调用 ThreadLocal 的 get()方法适合会触发其 initialValue 方法用来创建当前线程所需要的 SimpleDateFormat 对象。另外需要注意的是代码(4)使用完毕线程变量后要记得进行清理,以避免内存泄露。
Java并发编程笔记之SimpleDateFormat源码分析的更多相关文章
- Java并发编程笔记之CopyOnWriteArrayList源码分析
并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝 ...
- Java并发编程笔记之ThreadLocalRandom源码分析
JDK 并发包中 ThreadLocalRandom 类原理剖析,经常使用的随机数生成器 Random 类的原理是什么?及其局限性是什么?ThreadLocalRandom 是如何利用 ThreadL ...
- Java并发编程笔记之ThreadLocal源码分析
多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候, ...
- Java并发编程笔记之FutureTask源码分析
FutureTask可用于异步获取执行结果或取消执行任务的场景.通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过Fu ...
- Java并发编程笔记之Timer源码分析
timer在JDK里面,是很早的一个API了.具有延时的,并具有周期性的任务,在newScheduledThreadPool出来之前我们一般会用Timer和TimerTask来做,但是Timer存在一 ...
- Java并发编程笔记之CyclicBarrier源码分析
JUC 中 回环屏障 CyclicBarrier 的使用与分析,它也可以实现像 CountDownLatch 一样让一组线程全部到达一个状态后再全部同时执行,但是 CyclicBarrier 可以被复 ...
- Java并发编程笔记之PriorityBlockingQueue源码分析
JDK 中无界优先级队列PriorityBlockingQueue 内部使用堆算法保证每次出队都是优先级最高的元素,元素入队时候是如何建堆的,元素出队后如何调整堆的平衡的? PriorityBlock ...
- Java并发编程笔记之ArrayBlockingQueue源码分析
JDK 中基于数组的阻塞队列 ArrayBlockingQueue 原理剖析,ArrayBlockingQueue 内部如何基于一把独占锁以及对应的两个条件变量实现出入队操作的线程安全? 首先我们先大 ...
- Java并发编程笔记之ReentrantLock源码分析
ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞后放入该锁的AQS阻塞队列里面. 首先我们先看一下ReentrantLock的类图结构,如下图所示 ...
随机推荐
- MySQL开发——【数据的基本操作】
增加数据 基本语法: insert into 数据表 [字段名称1,字段名称2..] values (数据1,数据2...); 特别注意:针对数据类型整型.浮点型数据可以不加单引或双引号,但是如果字段 ...
- Hibernate Generic DAO的介绍安装和使用
java 的包挺多,比c#多 . jar包一个名,解压缩出来又出来又叫另一个名 .搜索起来,内容都分散的很 http://mvnrepository.com maven库搜索 com.googlec ...
- json-server使用及路由配置
1.先安装node.js,node.js中包含了json-server模块 2.在angular-hello/src/app/data-base.json文件中,编辑json格式的服务数据, { &q ...
- css初始
css概念及作用 css即层叠样式表的英文缩写 作用:1 渲染页面 2 页面布局 css语法 CSS 规则由两个主要的部分构成:选择器,以及一条或多条声明. 格式: selector{ prope ...
- Spring MVC 中的输入验证 Vlidator
在 Spring MVC 中有两种方式可以验证输入:1. Spring 自带的验证框架:2. 利用 JSR 303 实现,即 Java Specification Requests Converter ...
- Java第三次实验敏捷开发与XP实验
实验三-1 1.实验要求: 实验三 敏捷开发与XP实践 http://www.cnblogs.com/rocedu/p/4795776.html, Eclipse的内容替换成IDEA 参考 http: ...
- I/O dempo
标准读取写入 package io_stream; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; i ...
- Android 多线程编程
Android 多线程编程 //1.多线程 进程:操作系统的多道程序 线程:同一个程序的多条路径 //2.创建多线程程序 创建一个类extends Thread 重写run方法 在main方法中创建对 ...
- jquery中siblings方法配合什么方法一起使用
siblings() 获得匹配集合中每个元素的同胞,通过选择器进行筛选是可选的.接下来通过本文给大家介绍jQuery siblings()用法实例详解,需要的朋友参考下吧 siblings() 获得匹 ...
- selenium_unittest框架,TestCase引用
新手,纯属个人理解,有问题可以给出建议奥~谢谢. 如以下代码,每一个test的类都是一个测试方法而测试方法必须由test_xxx开头命名,非test开头可能会执行不到,执行顺序如test1,test2 ...