点击上方“Java基基”,选择“设为星标”

做积极的人,而不是积极废人!

每天 14:00 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:blog.csdn.net/QiuHaoqian/

article/details/116594422

9cd711ab375239e740e0bb62ca21259f.png


SimpleDateFormat在多线程环境下存在线程安全问题。

1 SimpleDateFormat.parse() 方法的线程安全问题

1.1 错误示例

错误使用SimpleDateFormat.parse()的代码如下:

import java.text.SimpleDateFormat;

public class SimpleDateFormatTest {
    private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {

        /**
         * SimpleDateFormat线程不安全,没有保证线程安全(没有加锁)的情况下,禁止使用全局SimpleDateFormat,否则报错 NumberFormatException
         *
         * private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         */
        for (int i = 0; i < 20; ++i) {
            Thread thread = new Thread(() -> {
                try {
                    // 错误写法会导致线程安全问题
                    System.out.println(Thread.currentThread().getName() + "--" + SIMPLE_DATE_FORMAT.parse("2020-06-01 11:35:00"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Thread-" + i);
            thread.start();
        }
    }
}

报错:

d310b5a5cb149adc2f278ae322264c14.png

1.2 非线程安全原因分析

查看源码中可以看到:SimpleDateFormat继承DateFormat类,SimpleDateFormat转换日期是通过继承自DateFormat类的Calendar对象来操作的,Calendar对象会被用来进行日期-时间计算,既被用于format方法也被用于parse方法。

3d8799aefe43ba727efbb37b8b413398.png

SimpleDateFormatparse(String source) 方法 会调用继承自父类的 DateFormatparse(String source) 方法

275e3fcafa9ca2cbbc9ff71bbb8c5fbb.png

DateFormatparse(String source) 方法会调用SimpleDateFormat中重写的 parse(String text, ParsePosition pos) 方法,该方法中有个地方需要关注

5bbc8f695f965cf195be360d7253c174.png

SimpleDateFormat 中重写的 parse(String text, ParsePosition pos) 方法中调用了 establish(calendar) 这个方法:

dcb0a3798ea734bceb308b126657717f.png

该方法中调用了 Calendarclear() 方法

7953233046617d99920c0b5b664af92d.png

可以发现整个过程中Calendar对象它并不是线程安全的,如果,a线程将calendar清空了,calendar 就没有新值了,恰好此时b线程刚好进入到parse方法用到了calendar对象,那就会产生线程安全问题了!

正常情况下:

ceaf6e589e9b8d571242cfafb30212c6.png

非线程安全的流程:

a95afed74c2e27b00f3e7d6dab52f491.png

1.3 解决方法

方法1:每个线程都new一个SimpleDateFormat

import java.text.SimpleDateFormat;

public class SimpleDateFormatTest {

    public static void main(String[] args) {
        for (int i = 0; i < 20; ++i) {
            Thread thread = new Thread(() -> {
                try {
                    // 每个线程都new一个
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    System.out.println(Thread.currentThread().getName() + "--" + simpleDateFormat.parse("2020-06-01 11:35:00"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Thread-" + i);
            thread.start();
        }
    }
}

方式2:synchronized等方式加锁

public class SimpleDateFormatTest {
    private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {

        for (int i = 0; i < 20; ++i) {
            Thread thread = new Thread(() -> {
                try {
                    synchronized (SIMPLE_DATE_FORMAT) {
                        System.out.println(Thread.currentThread().getName() + "--" + SIMPLE_DATE_FORMAT.parse("2020-06-01 11:35:00"));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Thread-" + i);
            thread.start();
        }
    }
}

方式3:使用ThreadLocal 为每个线程创建一个独立变量

import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class SimpleDateFormatTest {

    private static final ThreadLocal<DateFormat> SAFE_SIMPLE_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static void main(String[] args) {

        for (int i = 0; i < 20; ++i) {
            Thread thread = new Thread(() -> {
                try {
                        System.out.println(Thread.currentThread().getName() + "--" + SAFE_SIMPLE_DATE_FORMAT.get().parse("2020-06-01 11:35:00"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Thread-" + i);
            thread.start();
        }
    }
}

ThreadLocal的详细使用细节见:

https://blog.csdn.net/QiuHaoqian/article/details/117077792

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

2 SimpleDateFormat.format() 方法的线程安全问题

2.1 错误示例

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class SimpleDateFormatTest {
    // 时间格式化对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

    public static void main(String[] args) throws InterruptedException {
        // 创建线程池执行任务
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));

        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            // 执行任务
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    Date date = new Date(finalI * 1000); // 得到时间对象
                    formatAndPrint(date); // 执行时间格式化
                }
            });
        }
        threadPool.shutdown(); // 线程池执行完任务之后关闭
    }

    /**
     * 格式化并打印时间
     */
    private static void formatAndPrint(Date date) {
        String result = simpleDateFormat.format(date); // 执行格式化
        System.out.println("时间:" + result); // 打印最终结果
    }
}
737ad29ad0a8d0e627dd67ac0e604f55.png

从上述结果可以看出,程序的打印结果竟然有重复内容的,正确的情况应该是没有重复的时间才对。

2.2 非线程安全原因分析

为了找到问题所在,查看 SimpleDateFormatformat 方法的源码来排查一下问题,format 源码如下:

b92f6cb40123e777894192b001b45671.png

从上述源码可以看出,在执行 SimpleDateFormat.format() 方法时,会使用 calendar.setTime() 方法将输入的时间进行转换,那么我们想想一下这样的场景:

  • 线程 1 执行了 calendar.setTime(date) 方法,将用户输入的时间转换成了后面格式化时所需要的时间;

  • 线程 1 暂停执行,线程 2 得到 CPU 时间片开始执行;

  • 线程 2 执行了 calendar.setTime(date) 方法,对时间进行了修改;

  • 线程 2 暂停执行,线程 1 得出 CPU 时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当线程 1 继续执行的时候就会出现线程安全的问题了。

正常的情况下,程序的执行是这样的:

4b0d3a80e805c60d636bd17440469f2a.png

非线程安全的执行流程是这样的:

0f2ead157be413ec5ca995f65d8daffd.png

2.3 解决方法

同样有三种解决方法

方法1:每个线程都new一个SimpleDateFormat

public class SimpleDateFormatTest {
   
    public static void main(String[] args) throws InterruptedException {
        // 创建线程池执行任务
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));

        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            // 执行任务
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // 得到时间对象
                    Date date = new Date(finalI * 1000);
                    // 执行时间格式化
                    formatAndPrint(date);
                }
            });
        }
        // 线程池执行完任务之后关闭
        threadPool.shutdown();
    }

    /**
     * 格式化并打印时间
     */
    private static void formatAndPrint(Date date) {
        String result = new SimpleDateFormat("mm:ss").format(date); // 执行格式化
        System.out.println("时间:" + result); // 打印最终结果
    }
}

方式2:synchronized等方式加锁

所有的线程必须排队执行某些业务才行,这样无形中就降低了程序的运行效率了

public class SimpleDateFormatTest {
    // 时间格式化对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

    public static void main(String[] args) throws InterruptedException {
        // 创建线程池执行任务
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));

        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            // 执行任务
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    Date date = new Date(finalI * 1000); // 得到时间对象
                    formatAndPrint(date); // 执行时间格式化
                }
            });
        }
        // 线程池执行完任务之后关闭
        threadPool.shutdown();
    }

    /**
     * 格式化并打印时间
     */
    private static void formatAndPrint(Date date) {
        // 执行格式化
        String result = null;
        // 加锁
        synchronized (SimpleDateFormatTest.class) {
            result = simpleDateFormat.format(date);
        }
        // 打印最终结果
        System.out.println("时间:" + result);
    }
}

方式3:使用ThreadLocal 为每个线程创建一个独立变量

public class SimpleDateFormatTest {
    // 创建 ThreadLocal 并设置默认值
    private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));

    public static void main(String[] args) {
        // 创建线程池执行任务
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
        // 执行任务
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            // 执行任务
            threadPool.execute(() -> {
                Date date = new Date(finalI * 1000); // 得到时间对象
                formatAndPrint(date); // 执行时间格式化
            });
        }
        threadPool.shutdown(); // 线程池执行完任务之后关闭
    }

    /**
     * 格式化并打印时间
     */
    private static void formatAndPrint(Date date) {
        String result = dateFormatThreadLocal.get().format(date); // 执行格式化
        System.out.println("时间:" + result);  // 打印最终结果
    }
}


欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

6b5bbb87a33874ed8f3a236adbecbcaf.png

已在知识星球更新源码解析如下:

0f8f9a6733f10e3b9291df7c70c09cb5.png

050794476db9733c8bde86a99c2f0287.png

2d83f45e8407a1f73e572be558b6dade.png

a2122364da06dd872a3dc9aa26af3c7b.png

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 6W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐