多线程安全时间格式化操作

SimpleDateFormat是一个非线程安全的实现。从以下代码可以体现。

1.  package org.saxing;  

2.    

3.  import java.text.ParseException;  

4.  import java.text.SimpleDateFormat;  

5.  import java.util.Date;  

6.  import java.util.HashMap;  

7.  import java.util.Map;  

8.  import java.util.concurrent.*;  

9.    

10. /** 

11.  * SimpleDateFormat问题 

12.  * 

13.  * Created by saxing on 2018/3/19. 

14.  */  

15. public class DateUtil {  

16.   

17.     private static Map<String, SimpleDateFormat> ps = new HashMap<>();  

18.   

19.     private static SimpleDateFormat getSdf1(String pattern){  

20.         SimpleDateFormat s = ps.get(pattern);  

21.         if (s == null){  

22.             s = new SimpleDateFormat(pattern);  

23.             ps.put(pattern, s);  

24.         }  

25.         return s;  

26.     }  

27.   

28.     public static String format(Date date, String pattern){  

29.         return getSdf1(pattern).format(date);  

30.     }  

31.   

32.     public static Date parse(String dateString, String pattern) throws ParseException {  

33.         return getSdf1(pattern).parse(dateString);  

34.     }  

35.   

36.     public static void main(String[] args) throws ParseException {  

37.         String dateStr1 = "2018-03-19 23:29:34";  

38.         String dateStr2 = "2017-03-19";  

39.         String pattern1 = "yyyy-MM-dd HH:mm:ss";  

40.         String pattern2 = "yyyy-MM-dd";  

41.         int fixedNum = 4;  

42.         int threadNum = 9999;  

43.   

44.         Runnable runnable = () -> {  

45.             try {  

46.                 String resStr1 = DateUtil.format(DateUtil.parse(dateStr1, pattern1), pattern1);  

47.                 if (!dateStr1.equals(resStr1)){  

48.                     System.out.println("error\t resStr1: " + resStr1 + " dateStr1: " + dateStr1);  

49.                 }  

50.                 String resStr2 = DateUtil.format(DateUtil.parse(dateStr2, pattern2), pattern2);  

51.                 if (!dateStr2.equals(resStr2)){  

52.                     System.out.println("error\t resStr1: " + resStr2 + " dateStr2: " + dateStr2);  

53.                 }  

54.             } catch (ParseException e) {  

55.                 e.printStackTrace();  

56.             }  

57.         };  

58.   

59.         ExecutorService cachedPool = Executors.newCachedThreadPool();  

60.         ExecutorService fixedPool = Executors.newFixedThreadPool(fixedNum);  

61.         fixedPool.execute(() -> {  

62.             for (int i = 0; i < threadNum; i++){  

63.                 cachedPool.execute(runnable);  

64.             }  

65.         });  

66.     }  

67. }  


以上代码多次运行, 便会出现以下结果:


上述代码有如下几个问题:

1. SimpleDateFormat的format()、parse()为非线程安全,且没有做限制;

2. HashMap为非线程安全,且没有做限制;


SimpleDateFormat的format()、parse()为非线程安全,且没有做限制;

先看下SimpleDateFormat的源码:

1.  |- java.lang.Object  

2.     |- java.text.Format  

3.         |- java.text.DateFormat  

4.            |- java.text.SimpleDateFormat  


图1

从中可分析出,SimpleDateFormat的操作中有对calendar对象进行清除和获取操作。那么问题来了,如果在A线程set(time)之后,B线程进行了clear()操作,那A线程进行getTime()时,便会获取到“Thu Jan 01 00:00:00 CST 1970”,产生错误。


图2

解决此问题有如下:

方案一: 使用局部变量

每次都new一个SimpleDateFormat对象。

代码更改如下:

1.  private static SimpleDateFormat getSdf(String pattern){  

2.      return new SimpleDateFormat(pattern);  

3.  }  



图3

这种方法虽然简单,但是会引发频繁的GC。


图4

方案二:使用同步代码块

1.  private static final Object LOCK_OBJ = new Object();  

2.    

3.  private static final Map<String, SimpleDateFormat> PS = new HashMap<>();  

4.    

5.  private static SimpleDateFormat getSdf1(String pattern){  

6.      SimpleDateFormat s = PS.get(pattern);  

7.      if (s == null){  

8.          s = new SimpleDateFormat(pattern);  

9.          PS.put(pattern, s);  

10.     }  

11.     return s;  

12. }  

13.   

14. public static String format(Date date, String pattern){  

15.     synchronized (LOCK_OBJ){  

16.         String str = getSdf1(pattern).format(date);  

17.         return str;  

18.     }  

19. }  

20.   

21. public static Date parse(String dateString, String pattern) throws ParseException {  

22.     synchronized (LOCK_OBJ){  

23.         Date date = getSdf1(pattern).parse(dateString);  

24.         return date;  

25.     }  

26. }  


经测试99万线程并发没有产生错误。同时看一下性能:


图5

对比图4可以看出, gc次数和gc时间都有非常明显的下降。

Synchronized原理很简单,简单分析一下代码:


图6

锁对象有一个监视器锁(monitor),当一个线程执行monitorenter时,便会获取锁,同时此监视器内部计数器加1,当同一个进程多次进入此monitor时,monitor内计数器会多次加1。此时其他线程无法再获取此monitor;当monitorexit时,监视器内部计数器减1,直到减至0,其他被此monitor阻塞线程才可以竞争获取此锁。图中有两个monitorexit, 第二个monitorexit是为了确认锁已退出。


方案三:使用ThreadLocal(弃)

先说明此方案不推荐使用,或者是我没有找到好方法。仅介绍一下。不喜欢看这节的可以跳过。

       虽然方案二的Synchronized解决了并发线程安全问题,但是效率并不高。


图7

这时候可以了解ThreadLocal。以下为ThreadLocal在源码里的定义。

This class provides thread-local variables.  These variables differ from their normalcounterparts in that each thread that accesses one (via its {@code get} or{@code set} method) has its own, independently initialized copy of thevariable.  {@code ThreadLocal} instancesare typically private static fields in classes that wish to associate statewith a thread (e.g.,  a user ID orTransaction ID).
    大意为ThreadLocal是为线程提供局部变量,每个变量都有自己的副本。这种变量在多线程下访问时,能保证各个线程里的变量(通过get()/set()访问)独立于其他线程里的变量。ThreadLocal通常声明为private static类型,用于关联一个线程。

1.  private static ThreadLocal<Map<String, SimpleDateFormat>> threadLocal = new ThreadLocal(){  

2.      @Override  

3.      protected Object initialValue() {  

4.          return new HashMap<>();  

5.      }  

6.  };  

7.    

8.  private static SimpleDateFormat getSdf1(String pattern){  

9.      Map<String, SimpleDateFormat> map = threadLocal.get();  

10.     SimpleDateFormat df = map.get(pattern);  

11.     if (df == null){  

12.         df = new SimpleDateFormat(pattern);  

13.         map.put(pattern, df);  

14.         threadLocal.set(map);  

15.     }  

16.     return df;  

17. }  

18.   

19. public static String format(Date date, String pattern){  

20.     String str = getSdf1(pattern).format(date);  

21.     return str;  

22. }  

23.   

24. public static Date parse(String dateString, String pattern) throws ParseException {  

25.     Date date = getSdf1(pattern).parse(dateString);  

26.     return date;  

27. }  

28.   


经过99万线程并发测试,没有错误。

同时性能方面的测试和图7代码一样,但是结果如下:

图8

运算时间结果相差很大,明显使用了ThreadLocal性能卓越的多。当然简单的SimpleDateFormat一般也不会引起性能瓶颈,所以这种用法是否采用并不太重要。

       然而代码还是有问题的——ThreadLocal用法问题。前文定义里说明了ThreadLocal是为线程提供局部变量的,那么set的也是局部变量,无法让下一个线程取出上一个线程已经put入map的变量。那该怎么解决?答案是把局部变量变成全局变量。代码做如下演化:

1.  private static final Map<String, ThreadLocal<SimpleDateFormat>> SM = new HashMap<>();  

2.    

3.  private static SimpleDateFormat getSdf1(String pattern){  

4.      ThreadLocal<SimpleDateFormat> tl = SM.get(pattern);  

5.      if (tl == null){  

6.          tl = ThreadLocal.withInitial(() -> new SimpleDateFormat(pattern));  

7.          SM.put(pattern, tl);  

8.      }  

9.      return tl.get();  

10. }  


代码还是有问题,这时候就回到本文开头第二点问题。HashMap.class是非线程安全的。当多线程并发时,容易出现一些问题。通过以下代码体现:

1.  public class TestHashMap {  

2.    

3.      public static Map<Integer, Integer> map = new HashMap<>();  

4.      private static final int THREAD_NUM = 1000;  

5.    

6.      public static void main(String[] args) {  

7.          ExecutorService fixedPool = Executors.newFixedThreadPool(4);  

8.          fixedPool.execute(() -> {  

9.              for (int i = 1; i < THREAD_NUM; i++) {  

10.                 int temp = i;  

11.                 new Thread(() -> {  

12.                     map.put(temp, temp);  

13.                 }).start();  

14.             }  

15.             System.out.println("Insert Over.");  

16.             try {  

17.                 Thread.sleep(5000);  

18.             } catch (InterruptedException e) {  

19.                 e.printStackTrace();  

20.             }  

21.             readMap();  

22.         });  

23.     }  

24.   

25.     private static void readMap(){  

26.         for (int i = 1; i < THREAD_NUM; i++) {  

27.             Integer val = map.get(i);  

28.             if (val == null){  

29.                 System.out.println(i + ": lost data");  

30.             }  

31.         }  

32.         System.out.println("read over");  

33.     }  

34. }  

结果为:

1.  Insert Over.  

2.  117: lost data  

3.  121: lost data  

4.  123: lost data  

5.  127: lost data  

6.  330: lost data  

7.  read over 


关于HashMap的内部实现及jdk1.8改进了哪些,大有可讲,不在本文缀述。总之这里需要做对应的控制。方法有两点,要么用synchronized,要么用ConcurrentHashMap。改进代码如下:

1.  private static final Object LOCK_OBJ = new Object();  

2.  private static final Map<String, ThreadLocal<SimpleDateFormat>> SM = new HashMap<>();  

3.    

4.  private static SimpleDateFormat getSdf1(String pattern){  

5.      ThreadLocal<SimpleDateFormat> tl = SM.get(pattern);  

6.      if (tl == null){  

7.          synchronized (LOCK_OBJ){  

8.              tl = SM.computeIfAbsent(pattern, p -> ThreadLocal.withInitial(() -> new SimpleDateFormat(p)));  

9.          }  

10.     }  

11.     return tl.get();  

12. }  

或者:

1.  private static final Map<String, ThreadLocal<SimpleDateFormat>> SM = new ConcurrentHashMap<>();  

2.    

3.  private static SimpleDateFormat getSdf1(String pattern){  

4.      ThreadLocal<SimpleDateFormat> tl = SM.get(pattern);  

5.      if (tl == null){  

6.          tl = ThreadLocal.withInitial(() -> new SimpleDateFormat(pattern));  

7.          SM.putIfAbsent(pattern, tl);  

8.      }  

9.      return tl.get();  

10.

下面就要说这个ThreadLocal的坑爹地方了。

当我查看他的性能损耗时,意外的发现和普通的new SimpleDateFormat()相差不大。Excuse me?

后来我在代码里加这个行:
    

1.  private static SimpleDateFormat getSdf1(String pattern){  

2.      ThreadLocal<SimpleDateFormat> tl = SM.get(pattern);  

3.      if (tl == null){  

4.          System.out.println("tl == null");  

5.          tl = ThreadLocal.withInitial(() -> {  

6.              System.out.println("new pattern: " + pattern);  

7.              return new SimpleDateFormat(pattern);  

8.          });  

9.          SM.putIfAbsent(pattern, tl);  

10.     }  

11.     return tl.get();  

}  
输出结果里发现居然每次使用的时候都会去初始化一遍SimpleDateFormat。虽然是解决了效率问题,但是还不如一个方案一来得方便。所以我放弃了这种写法。

方案四:使用Joda-Time

大名鼎鼎,不多介绍,直接上代码:

1.  private static final Map<String, DateTimeFormatter> DM = new ConcurrentHashMap<>();  

2.    

3.  private static DateTimeFormatter getDf1(String pattern){  

4.      DateTimeFormatter df = DM.get(pattern);  

5.      if (df == null){  

6.          df = DateTimeFormat.forPattern(pattern);  

7.          DM.putIfAbsent(pattern, df);  

8.      }  

9.      return df;  

10. }  

11.   

12. public static String format(Date date, String pattern){  

13.     return new DateTime(date).toString(pattern);  

14. }  

15.   

16. public static Date parse(String dateString, String pattern) throws ParseException {  

17.     return DateTime.parse(dateString, getDf1(pattern)).toDate();  

18. }  


方案五:JDK8自带时间API

在Java1.0中,对时间的支持只能依赖java.util.Date类。此类无法表示日期,只能以毫秒精度表示时间。更糟糕的是它的易用性,比如起始时间从1900年开始,月份从0开始,toString()方法包含JVM默认时区CET,但本身却并不支持时区。

       Java1.1面世后,Date类中很多方法被废弃,出来新的java.util.Calendar类。情况仍然很糟,Calendar的月份仍然从0开始,与Date同时存在却不能共用,在某些场合需要互转等。如DateFormat只在Date里有,但是SimpleDateFormat却是线程不安全的。

       终于Java8自带了优秀的时间API:java.time。


1. public class Java8TimeUtil1 {  
2.   
3.     private static final Map<String, DateTimeFormatter> M = new ConcurrentHashMap<>();  
4.   
5.     private static DateTimeFormatter getDtf(String pattern){  
6.         DateTimeFormatter dtf = M.get(pattern);  
7.         if (dtf == null){  
8.             DateTimeFormatter newDtf = DateTimeFormatter.ofPattern(pattern);  
9.             dtf = M.putIfAbsent(pattern, newDtf);  
10.             if (dtf == null){  
11.                 dtf = newDtf;  
12.             }  
13.         }  
14.         return dtf;  
15.     }  
16.   
17.     public static LocalDateTime parse(String timeString, String pattern){  
18.         return LocalDateTime.parse(timeString, getDtf(pattern));  
19.     }  
20.   
21.     public static String format(LocalDateTime localDateTime, String pattern){  
22.         return localDateTime.format(getDtf(pattern));  
23.     }  
24.   
25.     public static void main(String[] args) {  
26.         String dateStr1 = "2017-04-29 23";  
27.         String pattern1 = "yyyy-MM-dd HH";  
28.         String dateStr2 = "2018-03-19 23:29:34";  
29.         String pattern2 = "yyyy-MM-dd HH:mm:ss";  
30.         final int thredNum = 999999;  
31.   
32.         Callable<String> task1 = () -> Java8TimeUtil1.format(Java8TimeUtil1.parse(dateStr1, pattern1), pattern1);  
33.         Callable<String> task2 = () -> Java8TimeUtil1.format(Java8TimeUtil1.parse(dateStr2, pattern2), pattern2);  
34.   
35.         ExecutorService threadPool = Executors.newFixedThreadPool(10);  
36.         for (int i = 0; i < thredNum; i++){  
37.             Future<String> future1 =  threadPool.submit(task1);  
38.             Future<String> future2 =  threadPool.submit(task2);  
39.             try {  
40.                 if (!dateStr1.equals(future1.get())){  
41.                     System.out.println("1 error: " + future1.get());  
42.                 }  
43.                 if (!dateStr2.equals(future2.get())){  
44.                     System.out.println("2 error: " + future2.get());  
45.                 }  
46.             } catch (InterruptedException | ExecutionException e) {  
47.                 e.printStackTrace();  
48.             }  
49.         }  
50.         System.out.println("over");  
51.     }  

52. }  


这也是现在我比较喜欢的方式。从运行过程看也是并行处理:

1. start: 2018-03-24T21:24:25.235  
2. 2018-03-24T21:24:25.248  
3. 2018-03-24T21:24:25.248  
4. 2018-03-24T21:24:26.259  
5. 2018-03-24T21:24:26.259  
6. 2018-03-24T21:24:27.260  
7. 2018-03-24T21:24:27.260  
8. 2018-03-24T21:24:28.261  
9. 2018-03-24T21:24:28.261  
10. 2018-03-24T21:24:29.261  
11. 2018-03-24T21:24:29.261  
12. 2018-03-24T21:24:30.262  
13. 2018-03-24T21:24:30.262  
14. 2018-03-24T21:24:31.263  
15. 2018-03-24T21:24:31.263  
16. 2018-03-24T21:24:32.264  
17. 2018-03-24T21:24:32.264  

18. over: 2018-03-24T21:24:33.264 


同时9999万并发对性能并不会有很大消耗:


图10
最后,在方案五里,getDtf()方法里关于putIfAbsent()的使用方法和前面都不一样。这里其实也是一个坑。请读者们自行学习研究吧。

小生才疏学浅,本文是随笔,错误请指出~ 

转载注明出处。

猜你喜欢

转载自blog.csdn.net/u011389515/article/details/79993787
今日推荐