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解决了并发线程安全问题,但是效率并不高。
这时候可以了解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).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代码一样,但是结果如下:运算时间结果相差很大,明显使用了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();
大名鼎鼎,不多介绍,直接上代码:
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万并发对性能并不会有很大消耗:
小生才疏学浅,本文是随笔,错误请指出~
转载注明出处。