追踪问题
网上找到的最多讨论的就是 calendar的线程不安全操作传递到了SimpleDateFormat
针对Calendar进行断点观察,观察其值的变化
观察calendar的赋值链
初始化赋值为:calendar = Calendar.getInstance(TimeZone.getDefault(), loc);
关键方法:format()
/**
* Formats a Date into a date/time string.
* @param date the time value to be formatted into a time string.
* @return the formatted time string.
*/
public final String format(Date date)
{
return format(date, new StringBuffer(),
DontCareFieldPosition.INSTANCE).toString();
}
calendar.setTime(date)
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date); // 把时间设置进Calendar对象内,生成需要转换成String 的元数据
// 省略其他分支代码
switch (tag) {
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
- 底层的subFormat() 大量进行calendar的读操作,这是线程不安全的核心
解释线程不安全
当A线程使用 SimpleDateFormat 对象 sa.format
-> calendar.setTime(date)
-> sa.subFormat()
读calendar操作
在线程A操作日期的调用链中,把对象sa
暴露给了线程B,线程B也执行了sa.format(date)
-> calendar.setTime(date)
, 写操作 此时线程A在进行读calendar操作,calendar内部的date已经被修改了,就会操作预期外的结果。
线程B破坏了线程A的上下文变量calendar的属性值,造成了线程不安全的后果
属于未保证原子性的情况
解决方案
BUG背景
为了减少 new SimpleDateFormat 对象的操作, 把 SimpleDateFormat 对象进行不安全的发布,如非同步容器HashMap。是一种内存优化导致的bug
解决思路
-
减少对象创建,并保持并发能力 – 线程池
使用Executors.newFixedThreadPool(10);
创建一个定容的线程池
因为最大线程数 == 核心线程数, SimpleDateFormat 对象并发执行,不会抢夺对象
只有一条线程释放了当前对象,下一个线程才有机会获得刚释放的对象,是一种无锁的操作,效率很高 -
线程隔离 – ThreadLocal
第一次调用即初始化,节省内存。class ThreadSafeFormatterWithLambda { public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); }
-
综上
同一时刻,最多只有10个线程,线程隔离得获取不同的SimpleDateFormat,并发格式化日期,
效益较好,同时避免了线程安全问题
代码
/**
* @Author james
* @Description
*
* 多线程使用 SimpleDateFormat 场景下,使用线程池 + ThreadLocal
*
* @Date 2019/12/26
*/
public class DateFormatWithThreadLocal {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(()-> {
String date = DateFormatWithThreadLocal.date(finalI);
System.out.println(date);
});
}
threadPool.shutdown();
}
public static String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatterWithLambda.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatterWithLambda {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal
= ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}