时间类变量在高并发环境下引发的线程安全问题

背景

生产环境中,登录接口出现偶发性的异常,排查发现是获取当前时间的工具类抛出异常,以下为代码片段:

/**
 * 时间工具类
 */
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(如 LocalDateLocalTimeLocalDateTime 和 DateTimeFormatter)。

2、CalendarCalendar 类也是线程不安全的。Java 8 推出了新的日期时间 API 来替代它,包括 LocalDateLocalTime 和 LocalDateTime 等。

3、Randomjava.util.Random 类在多线程环境下可能存在竞争条件,导致随机数生成不符合预期。为了解决这个问题,可以使用 java.util.concurrent.ThreadLocalRandom 类,它提供了线程安全的随机数生成。

4、DecimalFormatjava.text.DecimalFormat 类同样是线程不安全的。如果需要在多线程环境中进行数字格式化,可以考虑使用 ThreadLocal<DecimalFormat> 或将其作为局部变量。

5、StringBuilderjava.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;
    }
}
View Code

 

 

 

posted @ 2024-02-27 11:17  噗噗噗i丶  阅读(28)  评论(0编辑  收藏  举报