前言
SimpleDateFormat是 Java 提供的个格式化和解析日期的工具类,但是你是否在夜深人静的时候想过,自己通过SimpleDateFormat格式化日期的时候会不会出现线程安全方面的问题呢?
李四就是这样,清晨,第二天顶着两个硕大的黑眼圈写起了bug,不不不,是写起来代码。正所谓怕什么来什么,尽管李四昨晚一夜的祈祷不要代码出现问题,但始终没能避免王二越来越近的步伐…
李四的秘密
王二对着李四说道:
阿四啊,你最近干的不错,但是你有没有发现关于SimpleDateFormat的使用这个地方有问题呢
报告长官,暂时没有发现情况!
代码:
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++) {
new Thread(() -> {
try {
System.out.println(sdf.parse("2020-9-9 11:11:11"));
} catch (ParseException e) {
e.printStackTrace();
}
//线程启动 3
}).start();
}
}
}
王二:
阿四呀,我可是特意做了多线程处理,你居然还没发现问题,要是7米,早就一眼看出问题了,巴拉巴拉…
李四越听王二讲话,心里越不踏实,豆大的汗珠从额头冒了出来,在听到如果再不仔细干,绩效会被扣的时候,心里更是一紧,恐惧开始从李四的心底蔓延开来,于是李四为了不在发生这样的情况,他做出了一个重大的决定,周末约同事小红去动物园看老虎…
代码运行结果:
李四重启之问题分析
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();
// If the year value is ambiguous,
// then the two-digit year == the default start year
if (ambiguousYear[0]) {
if (parsedDate.before(defaultCenturyStart)) {
parsedDate = calb.addYear(100).establish(calendar).getTime();
}
}
}
// An IllegalArgumentException will be thrown by Calendar.getTime()
// if any fields are out of range, e.g., MONTH == 17.
catch (IllegalArgumentException e) {
pos.errorIndex = start;
pos.index = oldStart;
return null;
}
return parsedDate;
}
establish方法源码:
Calendar establish(Calendar cal) {
//3 重置日期对象cal的属性值
cal.clear();
//4 使用 calb中的属性值设置cal
...
//5 返回设置好的 cal 对象
return cal;
}
clear方法源码:
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;
}
从以上代码可以看出, 代码3、4、5不是原子性操作,当多个线程调用parse方法时,操作的是同一个cal对象,可能发生情况:
(1)返回数据又被clear清空的对象
(2)设置好的cal对象又被其它线程修改
李四终成之问题落幕
李四孤独的看完老虎后,痛定思痛,进过一番严肃认真的分析…
1.创建局部变量
每次使用时 new 一个SimpleDateFormat 实例,这样可以保证每个实例使自己的 Calendar 实例。
缺点:每次使用都需 new一个对象 ,并且使用后由于没有其他引用, 又需要回收,开销会很大
2.加锁
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++) {
new Thread(() -> {
try {
//加锁 3
synchronized (sdf) {
System.out.println(sdf.parse("2020-9-9 11:11:11"));
}
} catch (ParseException e) {
e.printStackTrace();
}
//线程启动 4
}).start();
}
}
}
缺点:高并发情况下性能较差,多个线程需要竞争锁,每次需要等待锁释放
3.使用ThreadLocal(推荐)
每个线程使用一个SimpleDateFormat实例。
class TestSimpleDateFormat {
//创建单例实例 1
static ThreadLocal<DateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static void main(String[] args) {
//创建多个线程2
for (int i = 0; i < 500; i++) {
new Thread(() -> {
try {
//单例解析 3
System.out.println(sdf.get().parse("2020-9-9 11:11:11"));
} catch (ParseException e) {
e.printStackTrace();
} finally {
//清除,避免内存泄露
sdf.remove();
}
//线程启动 5
}).start();
}
}
}
李四或许不在你的身边,但李四的故事会一直流传…
文章持续更新,可以微信搜索「 熊猫程序猿a 」第一时间催更