时间类变量在高并发环境下引发的线程安全问题
背景
生产环境中,登录接口出现偶发性的异常,排查发现是获取当前时间的工具类抛出异常,以下为代码片段:
/** * 时间工具类 */ public class DateUtil { Logger logger= LoggerFactory.getLogger(this.getClass()); private final static SimpleDateFormat shortSdf = new SimpleDateFormat("yyyy-MM-dd"); private final static SimpleDateFormat longSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private final static SimpleDateFormat sdfYMD = new SimpleDateFormat("yyyyMMdd"); private final static SimpleDateFormat sdfHM= new SimpleDateFormat("yyyyMMddHHmm"); private final static SimpleDateFormat sdf= new SimpleDateFormat("yyyy/MM/dd"); static SimpleDateFormat sdfYM = new SimpleDateFormat("yyyy-MM"); // 日期格式 …… /** * 获取当前日期 * * @return Date 返回类型 */ public static Date getNowDate() { Date date = null; try { date = shortSdf.parse(shortSdf.format(new Date())); } catch (ParseException e) { } return date; }
其中getNowDate()偶尔抛出异常:java.lang.NumberFormatException: For input string: ""
原因分析
SimpleDateFormat类本身是线程不安全的,当把SimpleDateFormat定义为类变量时,多个线程同时调用format和parse方法时,SimpleDateFormat的成员变量protected Calendar calendar
,会被多个线程同时访问或修改,导致线程安全问题。
format方法会调用:calendar.setTime(date);
parse方法会调用:CalendarBuilder类的establish方法,其中会调用cal.clear();
清空calendar实例的值。
解决方案
SimpleDateFormat
类确实是线程不安全的,如果将其定义为类变量并在多线程环境下使用,可能会导致错误。有几种方法可以解决这个问题:
1、局部变量:将 SimpleDateFormat
作为局部变量使用。这样每个线程都会创建一个新的对象,避免了线程安全问题。
public String formatDate(Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); return sdf.format(date); }
2、ThreadLocal:使用 ThreadLocal
可以让每个线程都拥有自己的 SimpleDateFormat
实例,从而避免线程安全问题。
private static final ThreadLocal<SimpleDateFormat> dateFormatter = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; public String formatDate(Date date) { return dateFormatter.get().format(date); }
3、同步块:使用同步块(synchronized block)来确保一次只有一个线程可以访问 SimpleDateFormat
,但这种方法可能会降低性能。
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); public String formatDate(Date date) { synchronized (sdf) { return sdf.format(date); } }
4、使用 Java 8 的 DateTimeFormatter:Java 8 引入了一套新的时间日期API,其中包括线程安全的 DateTimeFormatter
类。推荐升级到 Java 8 并使用这个类来解决线程安全问题。
import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public String formatDate(LocalDateTime date) { return dtf.format(date); }
以上四种方法都可以解决 SimpleDateFormat
的线程安全问题,可以根据项目需求和实际情况选择最适合的方法。
引申
在 JDK 1.8 中,主要有以下几个可能引发线程安全问题的类:
1、SimpleDateFormat: 如之前讨论的,SimpleDateFormat
类是非线程安全的。在处理日期和时间时,推荐使用 Java 8 的新日期时间 API(如 LocalDate
、LocalTime
、LocalDateTime
和 DateTimeFormatter
)。
2、Calendar: Calendar
类也是线程不安全的。Java 8 推出了新的日期时间 API 来替代它,包括 LocalDate
、LocalTime
和 LocalDateTime
等。
3、Random: java.util.Random
类在多线程环境下可能存在竞争条件,导致随机数生成不符合预期。为了解决这个问题,可以使用 java.util.concurrent.ThreadLocalRandom
类,它提供了线程安全的随机数生成。
4、DecimalFormat: java.text.DecimalFormat
类同样是线程不安全的。如果需要在多线程环境中进行数字格式化,可以考虑使用 ThreadLocal<DecimalFormat>
或将其作为局部变量。
5、StringBuilder: java.lang.StringBuilder
是线程不安全的,在多线程环境下应该使用 java.lang.StringBuffer
。但请注意,StringBuffer
的性能略低于 StringBuilder
,因此只有在确实需要共享缓冲区的情况下才应使用 StringBuffer
。
总之,在使用这些类时,需要注意线程安全性。在多线程环境下,可以考虑使用线程安全的替代方案。对于日期和时间处理,推荐使用 Java 8 的新日期时间 API。
dateutil.java示例:
package com.content.platform.backend.util; import org.apache.commons.lang.time.DateUtils; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.stream.Stream; /** * 日期工具类 */ public class DateUtil { private static final String DATE_FORMAT_PATTERN1 = "yyyyMMdd"; private static final String DATE_FORMAT_PATTERN2 = "yyyy-MM-dd"; /** * 计算两个日期之间相差天数 * * @param startDate 开始日期 * @param endDate 结束日期 * @return int */ public static int calculateDayCount(Date startDate, Date endDate) { Long dayLong = (endDate.getTime() - startDate.getTime()) / (1000 * 3600 * 24); return dayLong.intValue(); } /** * 计算两个日期之间相差天数 * * @param startDate 开始日期 * @param endDate 结束日期 * @return int * @throws ParseException */ public static int calculateDayCount(String startDate, String endDate) throws ParseException { Date parseStartDate = DateUtils.parseDate(startDate, new String[]{DATE_FORMAT_PATTERN1, DATE_FORMAT_PATTERN2}); Date parseEndDate = DateUtils.parseDate(endDate, new String[]{DATE_FORMAT_PATTERN1, DATE_FORMAT_PATTERN2}); Long dayLong = (parseStartDate.getTime() - parseEndDate.getTime()) / (1000 * 3600 * 24); return dayLong.intValue(); } /** * 将日期格式字符串转换为星期几的中文描述 * * @param date * @return String */ public static String transDateToWeek(String date) { String result = ""; Calendar calendar = Calendar.getInstance(); try { calendar.setTime(DateUtils.parseDate(date, new String[]{DATE_FORMAT_PATTERN1, DATE_FORMAT_PATTERN2})); } catch (ParseException e) { e.printStackTrace(); } int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); if (Calendar.MONDAY == dayOfWeek) { result = "星期一"; } if (Calendar.TUESDAY == dayOfWeek) { result = "星期二"; } if (Calendar.WEDNESDAY == dayOfWeek) { result = "星期三"; } if (Calendar.THURSDAY == dayOfWeek) { result = "星期四"; } if (Calendar.FRIDAY == dayOfWeek) { result = "星期五"; } if (Calendar.SATURDAY == dayOfWeek) { result = "星期六"; } if (Calendar.SUNDAY == dayOfWeek) { result = "星期日"; } return result; } /*** * 将yyyyMMdd转换成yyyy-MM-dd * @param strDate yyyyMMdd类型字符串 * @return yyyy-MM-dd类型字符串 */ public static String toFormatDate(String strDate) { try { Date date = new SimpleDateFormat(DATE_FORMAT_PATTERN1).parse(strDate); return new SimpleDateFormat(DATE_FORMAT_PATTERN2).format(date); } catch (ParseException e) { e.printStackTrace(); return null; } } /*** * 将yyyyMMdd转换成yyyy年MM月dd日 * @param strDate yyyyMMdd类型字符串 * @return yyyy-MM-dd类型字符串 */ public static String toFormatDate2(String strDate) { try { Date date = new SimpleDateFormat(DATE_FORMAT_PATTERN1).parse(strDate); return new SimpleDateFormat("yyyy.MM.dd").format(date); } catch (ParseException e) { e.printStackTrace(); return null; } } /** * 获取一段时间的每一天日期 * * @param start * @param end * @return * @throws Exception */ public static List<String> getBetweenDate(String start, String end) { DateTimeFormatter JEFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); LocalDate startDate = LocalDate.parse(start, JEFormatter); LocalDate endDate = LocalDate.parse(end, JEFormatter); List<String> list = new ArrayList<>(); long distance = ChronoUnit.DAYS.between(startDate, endDate); if (distance < 1) { return list; } Stream.iterate(startDate, d -> d.plusDays(1)).limit(distance + 1) .forEach(f -> list.add(f.format(JEFormatter))); return list; } /** * @Description 计算上季度末最后一天 yyyy-MM-dd * [date] * @Return java.lang.String * @Author zhy * @Date 2022/7/25 14:27 */ public static String preQuarterLastDay() { LocalDate localDate = LocalDate.now(); LocalDate lastDay = null; if (localDate.getMonthValue() == 1) { LocalDate lastMonth = LocalDate.of(localDate.getYear() - 1, 12, 1); lastDay = lastMonth.with(TemporalAdjusters.lastDayOfMonth()); } else { LocalDate lastMonth = LocalDate.of(localDate.getYear(), localDate.getMonthValue() - 1, 1); lastDay = lastMonth.with(TemporalAdjusters.lastDayOfMonth()); } return lastDay.toString(); } /** * @Description 装换格式为yyyy年MM月dd日 * [date] * @Return java.lang.String * @Author zhy * @Date 2022/7/25 14:27 */ public static String parseStyle(String pushTime) { LocalDate localDate = LocalDate.parse(pushTime); LocalDate lastDay = null; if (localDate.getMonthValue() == 1) { LocalDate lastMonth = LocalDate.of(localDate.getYear() - 1, 12, 1); lastDay = lastMonth.with(TemporalAdjusters.lastDayOfMonth()); } else { LocalDate lastMonth = LocalDate.of(localDate.getYear(), localDate.getMonthValue() - 1, 1); lastDay = lastMonth.with(TemporalAdjusters.lastDayOfMonth()); } ZoneId zone = ZoneId.systemDefault(); Instant instant = lastDay.atStartOfDay().atZone(zone).toInstant(); Date preDay = Date.from(instant); String parse = new SimpleDateFormat("yyyy年MM月dd日").format(preDay); return parse; } }