本文已参与「新人创作礼」活动.一起开启掘金创作之路。
场景:早高峰和下午15:00挂号,存在预约日期错乱,生产上频繁出现实际挂号日期和写入数据库日期不对等故障
分析:查看问题日志,发现出问题的数据都是在早上8:00-9:00 下午15:00左右,系统该段时间为放号时间,且除了预约日期,其他字段数据写入数据库都正确,猜测是跟多线程处理日期有关。
排查方式:经过单步程序调试发现不了问题,每次结果都正确。因此单独写了个定时器模拟多线程,1分钟启动一次程序处理日期,在关键地方打出日志并错误报警,SimpleDateFormat类处理后得到日期是1970-01-00,格林乔治系统初始时间。
来看 SimpleDateFormat 类的源码注释:
说的很清楚,SimpleDateFormat 不是线程安全的,多线程下需要为每个线程创建不同的实例。不安全的原因是因为使用了 Calendar 这个全局变量:
在日期格式化的时候:
这个 time 就会出现多线程并发设置安全问题,比如 A 线程在执行设置的时候,刚好被 B 线程抢先设置了,这样时间不就错乱了。
解决方法:
1)尽量使用局部变量;
2)如果要使用全局变量,则需要加锁格式化操作;
3)使用 ThreadLocal 进行线程隔离;
正确代码如下:
DateUtils.java 工具类
* 锁对象
*/
private static final Object lockObj = new Object();
/
* 存放不同的日期模板格式的sdf的Map
*
private static Map<String, ThreadLocal> sdfMap = new HashMap<String, ThreadLocal>();
**
* 多线程环境使用
*
-
*
- 方法描述:String类型转date类型 * @param dateStr 日期字符串如:2020-06-06
- 方法描述:date类型转String类型 \
* @param dateFormat 日期格式,如:yyyy-MM-dd
* @return java.util.Date
*/
public static Date parse(String dateStr, String dateFormat) {
try {
return getDateFormatNew(dateFormat).parse(dateStr);
} catch (Exception e) {
LOG.error("parse转换错误"+ dateStr); }
return null;
}
/** 多线程环境使用
*
*
* @param date 日期\
* @param dateFormat 日期格式,如:yyyy-MM-dd\
* @return java.lang.String 转换后的String日期\
*/
public static String format(Date date, String dateFormat) {
try {
return getDateFormatNew(dateFormat).format(date);
} catch (Exception e) {
LOG.error("format转换错误"); } return null; } private static SimpleDateFormat getDateFormatNew(final String pattern) {
ThreadLocal tl = sdfMap.get(pattern);
// 此处的双重判断和同步是为了防止sdfMap这个单例被多次put重复的sdf
if (tl == null) {
synchronized (lockObj) {
tl = sdfMap.get(pattern);
if (tl == null) {
// 只有Map中还没有这个pattern的sdf才会生成新的sdf并放入map
// 这里是关键,使用ThreadLocal替代原来直接new SimpleDateFormat tl = new ThreadLocal() { @Override\
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(pattern);
}
};
sdfMap.put(pattern, tl);
}
}
}
return tl.get();
}