• SimpleDateFormat 线程不安全及解决方案


    SimpleDateFormat定义

    SimpleDateFormat 是一个以与语言环境有关的方式来格式化和解析日期的具体类。它允许进行格式化(日期 -> 文本)、解析(文本 -> 日期)和规范化。
    
    SimpleDateFormat 使得可以选择任何用户定义的日期-时间格式的模式。但是,仍然建议通过 DateFormat 中的 getTimeInstance、getDateInstance 或 getDateTimeInstance 
    来创建日期-时间格式器。每一个这样的类方法都能够返回一个以默认格式模式初始化的日期/时间格式器。可以根据需要使用 applyPattern 方法来修改格式模式。

    官网同步建议

    同步
    日期格式是不同步的。建议为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须是外部同步的。

    为什么线程不安全

    上图中,SimpleDateFormat类中,有个对象calendar

    calendar 
              DateFormat 使用 calendar 来生成实现日期和时间格式化所需的时间字段值。

    当SimpleDateFormat用static申明,多个线程共享SimpleDateFormat对象是,也共享该对象的calendar对象。而当调用parse方法时,会clear所有日历字段和值。当线程A正在调用parse,线程B调用clear,这样解析后的数据就会出现偏差

    //parse方法
    @Override
    public Date parse(String text, ParsePosition pos)
    {
        try {
                parsedDate = calb.establish(calendar).getTime();
                ...
        }
    }


    //establish方法
    Calendar establish(Calendar cal) {
      ...  
      //将此 Calendar 的所有日历字段值和时间值(从历元至现在的毫秒偏移量)设置成未定义  
      cal.clear();  
    }

    同样 formart中也用到了calendar对象,将date设置到日历的时间字段中

     private StringBuffer format(Date date, StringBuffer toAppendTo,
                                    FieldDelegate delegate) {
            // Convert input date to time field list
            calendar.setTime(date);
            ...
    }

    当线程A调用setTime,而线程B也调用setTime,这时候线程A最后得到的时间是 最后线程B的时间。也会导致数据偏差

    不安全示例:

    public static void main(String[] args) throws InterruptedException {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateStr = "1111-11-11 11:11:11";
            ExecutorService executorService = Executors.newFixedThreadPool(100);
            for(int i=0;i<100;i++) {
                executorService.submit(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            //多个线程操作同一个sdf对象
                            System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName());
                        } catch (ParseException e) {
                            System.out.println("--------------> error, " + e.getMessage());
                        }
                    }
                });
    
            }
            executorService.shutdown();
        }                                                    

    执行结果:

    ...
    1111-11-11 11:11:11---pool-1-thread-69
    0011-11-11 11:11:11---pool-1-thread-72
    0011-11-11 11:11:11---pool-1-thread-71
    1111-11-11 11:11:11---pool-1-thread-73
    1111-11-11 11:11:11---pool-1-thread-75
    1111-11-11 11:11:11---pool-1-thread-76
    1111-11-11 11:11:11---pool-1-thread-89
    1111-11-11 00:11:11---pool-1-thread-93
    1111-11-11 11:11:11---pool-1-thread-96
    ...

    可以看到数据出现偏差

    解决方案

    1.为每个实例创建一个单独的SimpleDateFormat对象

    public static void main(String[] args) throws InterruptedException {
            //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateStr = "1111-11-11 11:11:11";
            ExecutorService executorService = Executors.newFixedThreadPool(100);
            for(int i=0;i<100;i++) {
                executorService.submit(new Runnable() {
                    //为每个线程创建自己的sdf对象
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    @Override
                    public void run() {
                        try {
                            System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName());
                        } catch (ParseException e) {
                            System.out.println("--------------> error, " + e.getMessage());
                        }
                    }
                });
    
            }
            executorService.shutdown();
        }

      缺点:每次new一个实例,都会new一个format对象,虚拟机内存消耗大,垃圾回收频繁

    2.给静态SimpleDateFormat对象加锁,使用Lock或者synchronized修饰

    public static void main(String[] args) throws InterruptedException {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateStr = "1111-11-11 11:11:11";
            ExecutorService executorService = Executors.newFixedThreadPool(100);
            for(int i=0;i<100;i++) {
                executorService.submit(new Runnable() {
                    @Override
                    public void run() {
                       //加同步锁
                        synchronized (sdf) {
                            try {
                                System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName());
                            } catch (ParseException e) {
                                System.out.println("--------------> error, " + e.getMessage());
                            }
                        }
                    }
                });
    
            }
            executorService.shutdown();
        }

      缺点:性能差,其他线程要等待锁释放

    3.使用ThreadLocal为每个线程创建一个SimpleDateFormat对象副本,有线程隔离性,各自的副本对象也不会被其他线程影响

    public static void main(String[] args) throws InterruptedException {
            //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            //初始化threadLocal并设置值
            ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){
                @Override
                protected SimpleDateFormat initialValue() {
                    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                }
            };
            String dateStr = "1111-11-11 11:11:11";
            ExecutorService executorService = Executors.newFixedThreadPool(100);
            for(int i=0;i<100;i++) {
                executorService.submit(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            System.out.println(threadLocal.get().format(threadLocal.get().parse(dateStr)) + "---" + Thread.currentThread().getName());
                        } catch (ParseException e) {
                            System.out.println("--------------> error, " + e.getMessage());
                        }
                    }
                });
    
            }
            executorService.shutdown();
            //清理threadLocal,生产环境不清理容易导致内存溢出
    threadLocal.remove();
    }

    ThreadLocal原理分析

  • 相关阅读:
    codevs2574 波兰表达式
    绿书模拟day10 单词前缀
    codevs2171 棋盘覆盖
    noip2008 双栈排序
    图论总结复习
    noip2010 关押罪犯
    flask使用geventwebsocket完成小型聊天系统
    MongoDB
    flask基础内容总览
    flask蓝图,cbv,python中的redis操作
  • 原文地址:https://www.cnblogs.com/yangzhenlong/p/8385061.html
Copyright © 2020-2023  润新知