• 项目记事【多线程】:关于 SimpledDateFormat 的多线程问题


    背景:

      最近项目引入了 SonarLink,解决代码规范的问题,在检查历史代码的时候,发现了一个问题。

      先看代码:

     1 public class DateUtil {
     2 
     3     private static final String DATE_FORMAT_1 = "yyyy-MM-dd HH:mm:ss";
     4     private static final String DATE_FORMAT_2 = "yyyy-MM-dd";
     5 
     6     private static SimpleDateFormat sdf1 = new SimpleDateFormat(DATE_FORMAT_1);
     7     private static SimpleDateFormat sdf2 = new SimpleDateFormat(DATE_FORMAT_2);
     8 
     9     private DateUtil() {
    10 
    11     }
    12 
    13     public static String formatDate1(Date date) throws ParseException {
    14         return sdf1.format(date);
    15     }
    16 
    17     public static String formatDate2(Date date) throws ParseException {
    18         return sdf2.format(date);
    19     }
    20 
    21     public static Date parseDate1(String dateStr) throws ParseException {
    22         return sdf1.parse(dateStr);
    23     }
    24 
    25     public static Date parseDate2(String dateStr) throws ParseException {
    26         return sdf2.parse(dateStr);
    27     }
    28 
    29 }
    DateUtil

      问题出在什么地方?就出在一个共享的变量 SimpledDateFormat,本身是一个线程不安全的类(由于内部实现使用了 Calendar),导致在多线程情况下可能出错。

    多线程检测:

     1 public class DateFormatTest {
     2 
     3     public static class TestSimpleDateFormatThreadSafe extends Thread {
     4 
     5         @Override
     6         public void run() {
     7             while (true) {
     8                 try {
     9                     this.join(1000);
    10                 } catch (InterruptedException e1) {
    11                     e1.printStackTrace();
    12                 }
    13                 try {
    14                     System.out.println(this.getName() + ":" + DateUtil.parseDate1("2013-05-24 06:02:20"));
    15                 } catch (ParseException e) {
    16                     e.printStackTrace();
    17                 }
    18             }
    19         }
    20     }
    21 
    22     public static void main(String[] args) {
    23         for (int i = 0; i < 3; i++) {
    24             new TestSimpleDateFormatThreadSafe().start();
    25         }
    26     }
    27 
    28 }
    DateFormatTest

      执行结果如下图(多次执行,出现的结果可能不同):

    解决方案:

      这种问题,不仅仅会出现在 SimpleDateFormat 中,只能说 SimpleDateFormat 比较常见,具有代表性。

      只要是将一个线程不安全类产生的实例作为共享变量,都有可能出现多线程的问题。

      出现这类问题,应该从以下3个角度考虑解决方案:

    1. 规避将一个线程不安全的对象,作为共享变量的情况。
    2. 从多线程的角度考虑,解决线程安全问题。
    3. 不使用线程不安全的对象,找一个拥有相同功能的其他类,作为替代方案。

    第一类解决方案:

      不要将线程不安全的对象,作为共享变量,在方法内部调用的时候再初始化这个对象。

      代码如下:

     1 public class DateUtil1 {
     2 
     3     private static final String DATE_FORMAT_1 = "yyyy-MM-dd HH:mm:ss";
     4     private static final String DATE_FORMAT_2 = "yyyy-MM-dd";
     5 
     6     private DateUtil1() {
     7 
     8     }
     9 
    10     public static String formatDate1(Date date) throws ParseException {
    11         SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_1);
    12         return sdf.format(date);
    13     }
    14 
    15     public static String formatDate2(Date date) throws ParseException {
    16         SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_2);
    17         return sdf.format(date);
    18     }
    19 
    20     public static Date parseDate1(String dateStr) throws ParseException {
    21         SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_1);
    22         return sdf.parse(dateStr);
    23     }
    24 
    25     public static Date parseDate2(String dateStr) throws ParseException {
    26         SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_2);
    27         return sdf.parse(dateStr);
    28     }
    29 
    30 }
    DateUtil1

      很显然,大多数情况下,都不会采用这个方案,频繁地创建-销毁对象,对于内存的影响非常大。

    第二类解决方案:

      从多线程的角度,就是说在使用这个线程不安全类的时候,加以控制,从以下两个角度入手:

    1. 时间换空间:同步锁 synchronized。
    2. 空间换时间:独立线程 ThreadLocal。

    synchronized

      使用 synchronized 的思路很简单,在使用线程不安全变量之前,先将这个变量用 synchronized 关键字锁住。

      每一次其他线程使用这个变量时,会等待上一个线程释放这个锁之后再执行。

      很明显,在高并发的情况下,这种方案对于时间的消耗很大。

      代码如下:

     1 public final class DateUtil2 {
     2 
     3     private static final String DATE_FORMAT_1 = "yyyy-MM-dd HH:mm:ss";
     4     private static final String DATE_FORMAT_2 = "yyyy-MM-dd";
     5 
     6     private static final SimpleDateFormat SDF_1 = new SimpleDateFormat(DATE_FORMAT_1);
     7     private static final SimpleDateFormat SDF_2 = new SimpleDateFormat(DATE_FORMAT_2);
     8 
     9     private DateUtil2() {
    10 
    11     }
    12 
    13     public static String formatDate1(Date date) throws ParseException {
    14         synchronized (SDF_1) {
    15             return SDF_1.format(date);
    16         }
    17     }
    18 
    19     public static String formatDate2(Date date) throws ParseException {
    20         synchronized (SDF_2) {
    21             return SDF_2.format(date);
    22         }
    23     }
    24 
    25     public static Date parseDate1(String dateStr) throws ParseException {
    26         synchronized (SDF_1) {
    27             return SDF_1.parse(dateStr);
    28         }
    29     }
    30 
    31     public static Date parseDate2(String dateStr) throws ParseException {
    32         synchronized (SDF_2) {
    33             return SDF_2.parse(dateStr);
    34         }
    35     }
    36 
    37 }
    DateUtil2

    ThreadLocal

      ThreadLocal,可以简单地这么理解,ThreadLocal 为每一个使用这个线程的变量创建一个独立的副本。

      每一个线程都在自己内部改变这个变量,自然不会出现线程安全问题。

      明显,这种方案对于内存空间,消耗极大。

      代码如下:

     1 public final class DateUtil3 {
     2 
     3     private static final String DATE_FORMAT_1 = "yyyy-MM-dd HH:mm:ss";
     4     private static final String DATE_FORMAT_2 = "yyyy-MM-dd";
     5 
     6     private static Map<String, ThreadLocal<SimpleDateFormat>> threadLocalMap;
     7     private static List<String> dateFormatStringList;
     8 
     9     private DateUtil3() {
    10 
    11     }
    12 
    13     static {
    14         threadLocalMap = new HashMap<>();
    15         dateFormatStringList = new ArrayList<>();
    16         dateFormatStringList.add(DATE_FORMAT_1);
    17         dateFormatStringList.add(DATE_FORMAT_2);
    18         for (final String s : dateFormatStringList) {
    19             SimpleDateFormat sdf = new SimpleDateFormat(s);
    20             ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>() {
    21                 @Override
    22                 protected SimpleDateFormat initialValue() {
    23                     return new SimpleDateFormat(s);
    24                 }
    25             };
    26             threadLocal.set(sdf);
    27             threadLocalMap.put(s, threadLocal);
    28         }
    29     }
    30 
    31     public static String formatDate1(Date date) throws ParseException {
    32         SimpleDateFormat sdf = threadLocalMap.get(DATE_FORMAT_1).get();
    33         return sdf.format(date);
    34     }
    35 
    36     public static String formatDate2(Date date) throws ParseException {
    37         SimpleDateFormat sdf = threadLocalMap.get(DATE_FORMAT_2).get();
    38         return sdf.format(date);
    39     }
    40 
    41     public static Date parseDate1(String dateStr) throws ParseException {
    42         SimpleDateFormat sdf = threadLocalMap.get(DATE_FORMAT_1).get();
    43         return sdf.parse(dateStr);
    44     }
    45 
    46     public static Date parseDate2(String dateStr) throws ParseException {
    47         SimpleDateFormat sdf = threadLocalMap.get(DATE_FORMAT_2).get();
    48         return sdf.parse(dateStr);
    49     }
    50 
    51 }
    DateUtil3

    第三类解决方案:

      

  • 相关阅读:
    官方文档:grep 命令
    进阶之路 守 破 离
    官方文档:gawk 或 awk 命令
    官方文档:sed 命令
    阿里云云效codeup代码管理上传本地项目
    [golang] 解决配置goland时候The selected directory is not a valid home for Go SDK
    [css] css实现文字竖向排列以及设置间距
    在线客服聊天系统源码_美观强大golang内核开发_二进制运行傻瓜式安装_附搭建教程
    在线客服系统代码_h5客服_对接公众号_支持APP_支持多语言
    如何给自己的网站接入在线客服系统代码
  • 原文地址:https://www.cnblogs.com/jing-an-feng-shao/p/7573220.html
Copyright © 2020-2023  润新知