项目记事【多线程】:关于 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

 

 

第三类解决方案:

 

  

 

posted @ 2017-09-22 09:33  Gerrard_Feng  阅读(260)  评论(0)    收藏  举报