# 《并发设计模式》第43章-线程特有存储模式-解决格式化时间的线程安全问题

作者:冰河
星球:http://m6z.cn/6aeFbs (opens new window)
博客:https://binghe.gitcode.host (opens new window)
文章汇总:https://binghe.gitcode.host/md/all/all.html (opens new window)
源码获取地址:https://t.zsxq.com/0dhvFs5oR (opens new window)

沉淀,成长,突破,帮助他人,成就自我。

  • 本章难度:★★☆☆☆
  • 本章重点:了解什么是线程特有存储模式,线程特有存储模式的应用场景,重点理解线程特有存储模式解决线程安全的核心思路与原理,能够融会贯通,并能够结合自身项目实际场景思考如何将线程特有存储模式灵活应用到自身实际项目中。

大家好,我是冰河~~

JDK中提供的SimpleDateFormat类本身不是线程安全的,在多线程并发场景下,使用SimpleDateFormat格式化日期和时间时,可能会出现线程安全的问题,而使用线程特有存储模式就能够解决SimpleDateFormat格式化日期和时间时的线程安全问题。

# 一、案例背景

提起SimpleDateFormat类,想必做过Java开发的童鞋都不会感到陌生。没错,它就是Java中提供的日期时间的转化类。这里,为什么说SimpleDateFormat类有线程安全问题呢?有些小伙伴可能会提出疑问:我们生产环境上一直在使用SimpleDateFormat类来解析和格式化日期和时间类型的数据,一直都没有问题啊!我的回答是:没错,那是因为你们的系统达不到SimpleDateFormat类出现问题的并发量,也就是说你们的系统没啥负载!

接下来,我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题。

# 二、重现线程安全问题

为了重现SimpleDateFormat类的线程安全问题,一种比较简单的方式就是使用线程池结合Java并发包中的CountDownLatch类和Semaphore类来重现线程安全问题。

有关CountDownLatch类和Semaphore类的具体用法和原理,大家可以阅读《深入理解高并发编程:核心原理与案例实战》和《深入理解高并发编程:JDK核心技术》书籍,这里不再赘述。

好了,先来看下重现SimpleDateFormat类的线程安全问题的代码,源码详见:io.binghe.concurrent.design.threadlocal.wrong.WrongSimpleDateFormat。

public class WrongSimpleDateFormat {

    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        simpleDateFormat.parse("9999-01-01");
                    } catch (ParseException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }catch (NumberFormatException e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

可以看到,在SimpleDateFormatTest01类中,首先定义了两个常量,一个是程序执行的总次数,一个是同时运行的线程数量。程序中结合线程池和CountDownLatch类与Semaphore类来模拟高并发的业务场景。其中,有关日期转化的代码只有如下一行。

simpleDateFormat.parse("9999-01-01");
1

当程序捕获到异常时,打印相关的信息,并退出整个程序的运行。当程序正确运行后,会打印“所有线程格式化日期成功”。

运行程序输出的结果信息如下所示。

线程:pool-1-thread-2 格式化日期失败
线程:pool-1-thread-6 格式化日期失败
线程:pool-1-thread-8 格式化日期失败
线程:pool-1-thread-1 格式化日期失败
线程:pool-1-thread-5 格式化日期失败
线程:pool-1-thread-3 格式化日期失败
线程:pool-1-thread-15 格式化日期失败
线程:pool-1-thread-14 格式化日期失败
线程:pool-1-thread-12 格式化日期失败
线程:pool-1-thread-17 格式化日期失败
线程:pool-1-thread-9 格式化日期失败
线程:pool-1-thread-11 格式化日期失败
线程:pool-1-thread-7 格式化日期失败
线程:pool-1-thread-16 格式化日期失败
java.lang.NumberFormatException: For input string: "..1111EE111199"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:578)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at io.binghe.concurrent.design.threadlocal.wrong.WrongSimpleDateFormat.lambda$main$0(WrongSimpleDateFormat.java:50)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at io.binghe.concurrent.design.threadlocal.wrong.WrongSimpleDateFormat.lambda$main$0(WrongSimpleDateFormat.java:50)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: empty String
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

说明,在高并发下使用SimpleDateFormat类格式化日期时抛出了异常,SimpleDateFormat类不是线程安全的!!!

接下来,我们就看下,SimpleDateFormat类为何不是线程安全的。

# 查看全文

加入冰河技术 (opens new window)知识星球,解锁完整技术文章与完整代码