SimpleDateFormat 线程不安全及解决方案详解编程语言

SimpleDateFormat定义

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

官网同步建议

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

为什么线程不安全

SimpleDateFormat 线程不安全及解决方案详解编程语言

 

上图中,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原理分析

 

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/16940.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论