• Java基础之SimpleDateFormat的多线程陷阱


            SimpleDateFormat类我们可太熟了。Date转String、String转Date,我们不可避免的用到SimpleDateFormat,而且用起来非常简单。

    1. public static void main(String[] args) throws ParseException {
    2. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    3. System.out.println(sdf.format(new Date()));
    4. String dateString = "2022-08-04 09:03:00";
    5. System.out.println(sdf.parse(dateString));
    6. }

            这样短短几行代码就搞定了从Date转String和从String转Date。这样方便易用的类,我们怎能不爱。对于经常使用Date类型转换的项目,我们会把整个Date转换封装成一个工具类。

    1. public class DateUtil {
    2. public static String dateToDateString(Date date) {
    3. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    4. return sdf.format(date);
    5. }
    6. public static Date dateStringToDate(String dateString) throws ParseException {
    7. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    8. return sdf.parse(dateString);
    9. }
    10. }

            这样每次创建一个SimpleDateFormat实例是不是不够节检,每次调用方法,创建实例也是一笔小小的开销,自古节俭都是美德,我把它优化成静态常量,可以创建一次全局调用,我可真是个小机灵。

    1. public class DateUtil {
    2. public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    3. public static String dateToDateString(Date date) {
    4. return sdf.format(date);
    5. }
    6. public static Date dateStringToDate(String dateString) throws ParseException {
    7. return sdf.parse(dateString);
    8. }
    9. }

            这时候我们就掉进了陷阱中,我们精心为自己设计的陷阱:在简单环境中这种写法不会出现问题,但是在公司业务渐入佳境,并发量上来的时候,非线程安全的问题就会暴露出来。

    Parse方法部分

    1. public class SimpleDateFormatTest {
    2. public static void main(String[] args) {
    3. String[] dateStr = {
    4. "2020-12-12 12:12:12",
    5. "2020-12-12 12:12:12",
    6. "2020-12-12 12:12:12",
    7. "2020-12-12 12:12:12",
    8. "2020-12-12 12:12:12",
    9. "2020-12-12 12:12:12"
    10. };
    11. //实在多线程的环境下会出现问题
    12. new Thread(() -> {
    13. for (String s : dateStr) {
    14. Date date = null;
    15. try {
    16. date = DateUtil.dateStringToDate(s);
    17. } catch (ParseException e) {
    18. e.printStackTrace();
    19. }
    20. System.out.println(Thread.currentThread().getName() + date);
    21. }
    22. }).start();
    23. new Thread(() -> {
    24. for (String s : dateStr) {
    25. Date date = null;
    26. try {
    27. date = DateUtil.dateStringToDate(s);
    28. } catch (ParseException e) {
    29. e.printStackTrace();
    30. }
    31. System.out.println(Thread.currentThread().getName() + date);
    32. }
    33. }).start();
    34. }
    35. }

            这段代码在执行的时候会出现异常。这个报错是提示我数字格式化异常,存在多个小数点。别问,问就是吃了没文化的亏。

            想知道为什么会产生这个错误,我们就需要深入源码查看一下了。我们调用的parse(String)经过调用一系列重载重写方法后,进入到SimpleDateFormatparse(String text, ParsePosition pos)方法。有些人抄来抄去说这块是Calendar的原因,但是其实这块还不是Calendar的原因。找到出问题的getDouble函数。 

            在SimpleDateFormatstatic修饰的时候,我们就共用了这个DigitList类的getDouble方法,在多线程场景下自然而然的发生很多奇怪的问题。 

    Format方法部分

            当然了,毕竟SimpleDateFormat类非线程安全,所以format方法也会有线程安全问题。而format方法的问题出在CalendarCalendarSimpleDateFormat私有变量。

    1. private StringBuffer format(Date date, StringBuffer toAppendTo,
    2. FieldDelegate delegate) {
    3. // Convert input date to time field list
    4. calendar.setTime(date);
    5. boolean useDateFormatSymbols = useDateFormatSymbols();
    6. for (int i = 0; i < compiledPattern.length; ) {
    7. int tag = compiledPattern[i] >>> 8;
    8. int count = compiledPattern[i++] & 0xff;
    9. if (count == 255) {
    10. count = compiledPattern[i++] << 16;
    11. count |= compiledPattern[i++];
    12. }
    13. switch (tag) {
    14. case TAG_QUOTE_ASCII_CHAR:
    15. toAppendTo.append((char)count);
    16. break;
    17. case TAG_QUOTE_CHARS:
    18. toAppendTo.append(compiledPattern, i, count);
    19. i += count;
    20. break;
    21. default:
    22. subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
    23. break;
    24. }
    25. }
    26. return toAppendTo;
    27. }

    解决办法

            既然我们知道了问题是怎样产生的,那么再来看一下问题需要怎么解决。

            1.使用ThreadLocal

                    我觉得使用ThreadLocal是最可以再面试中吹牛皮的。让面试官眼前一亮。

    1. public class DateUtil {
    2. // 优化后
    3. private static ThreadLocal threadLocal = new ThreadLocal(){
    4. @Override
    5. protected DateFormat initialValue() {
    6. return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    7. }
    8. };
    9. // 优化前
    10. // public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    11. public static String dateToDateString(Date date) {
    12. String dateString = threadLocal.get().format(date);
    13. // 记得remove,以免内存泄漏
    14. threadLocal.remove();
    15. return dateString;
    16. }
    17. public static Date dateStringToDate(String dateString) throws ParseException {
    18. Date date = threadLocal.get().parse(dateString);
    19. // 记得remove,以免内存泄漏
    20. threadLocal.remove();
    21. return date;
    22. }
    23. }

            2.在代码中加入synchronized方法块

                    可以解决问题,但不够秀。有点东西但不多!而且将日期转换变成单线程执行,多个线程争抢,会导致效率降低。

    1. public class SimpleDateFormatTest {
    2. public static void main(String[] args) {
    3. String[] dateStr = {
    4. "2020-12-12 12:12:12",
    5. "2020-12-12 12:12:12",
    6. "2020-12-12 12:12:12",
    7. "2020-12-12 12:12:12",
    8. "2020-12-12 12:12:12",
    9. "2020-12-12 12:12:12"
    10. };
    11. //实在多线程的环境下会出现问题
    12. new Thread(() -> {
    13. for (String s : dateStr) {
    14. Date date = null;
    15. try {
    16. synchronized (SimpleDateFormatTest.class) {
    17. date = DateUtil.dateStringToDate(s);
    18. }
    19. } catch (ParseException e) {
    20. e.printStackTrace();
    21. }
    22. System.out.println(Thread.currentThread().getName() + date);
    23. }
    24. }).start();
    25. new Thread(() -> {
    26. for (String s : dateStr) {
    27. Date date = null;
    28. try {
    29. synchronized (SimpleDateFormatTest.class) {
    30. date = DateUtil.dateStringToDate(s);
    31. }
    32. } catch (ParseException e) {
    33. e.printStackTrace();
    34. }
    35. System.out.println(Thread.currentThread().getName() + date);
    36. }
    37. }).start();
    38. }
    39. }

            3.使用DateTimeFormatter

                    DateTimeFormatter是JDK1.8开始对外提供服务的线程安全的日期格式化类。不确定面试官懂不懂这个类,没准可以装个比。

    1. public class DateUtil {
    2. // 优化前
    3. public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
    4. // 优化后
    5. public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    6. public static String dateToDateString(Date date) {
    7. return DATE_TIME_FORMATTER.format(LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()));
    8. }
    9. public static Date dateStringToDate(String dateString) throws ParseException {
    10. return Date.from(Instant.from(DATE_TIME_FORMATTER.parse(dateString)));
    11. }
    12. }
  • 相关阅读:
    指针面试问题
    关于antdpro的EdittableProTable编辑时两个下拉搜索框的数据联动以及数据回显(以及踩坑)
    深入理解.Net中的线程同步之构造模式(二)内核模式2.内核模式构造物Semaphone
    gitee page中HTML显示乱码
    SpringCloud学习笔记万字整理(无广版在博客)
    java: 自定义java.util.logging.Logger的日志输出格式,输出IDE(ECLIPSE)能自动识别行号的格式
    MySQL基本操作
    虚拟号码认证如何开通?
    【附源码】Python计算机毕业设计社区疫情防控监管系统
    redis实现未支付时间超时就删除订单,并给前端反应一个已过期
  • 原文地址:https://blog.csdn.net/qq_22156459/article/details/126231627