• SimpleDateFormat,Calendar 线程非安全的问题


    SimpleDateFormat是Java中非常常见的一个类,用来解析和格式化日期字符串。但是SimpleDateFormat在多线程的环境并不是安全的,这个是很容易犯错的部分,接下来讲一下这个问题出现的过程以及解决的思路。

    问题描述:
    先看代码,用来获取一个月的天数的:

    import java.text.SimpleDateFormat;
    import java.util.Calendar;
    import java.util.Date;
    
    public class DateUtil {
    
        /**
         * 获取月份天数
         * @param time 201202
         * @return
         */
        public static int getDays(String time) throws Exception {
    //        String time = "201202";
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
            Date date = sdf.parse(time);
            Calendar c = Calendar.getInstance();
            c.setTime(date);
            int day = c.getActualMaximum(Calendar.DATE);
            return day;
        }
        
    }

    可以看到在这个方法里,每次要获取值的时候就先要创建一个SimpleDateFormat的实例,频繁调用这个方法的情况下很耗性能。为了避免大量实例的频繁创建和销毁,我们通常会使用单例模式或者静态变量进行改造,一般会这么改:

    import java.text.SimpleDateFormat;
    import java.util.Calendar;
    import java.util.Date;
    
    public class DateUtil {
    
            private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
    
        /**
         * 获取月份天数
         * @param time 201202
         * @return
         */
        public static int getDays(String time) throws Exception {
    //        String time = "201202";        
            Date date = sdf.parse(time);
            Calendar c = Calendar.getInstance();
            c.setTime(date);
            int day = c.getActualMaximum(Calendar.DATE);
            return day;
        }
        
    }

    此时不管调用多少次这个方法,java虚拟机里只有一个SimpleDateFormat对象,效率和性能肯定要比第一个方法好,这个也是很多程序员选择的方法。但是,在这个多线程的条件下,多个thread共享同一个SimpleDateFormat,而SimpleDateFormat本身又是线程非安全的,这样就很容易出各种问题。

    验证问题:
    用一个简单的例子验证一下多线程环境下SimpleDateFormat的运行结果:

    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.CountDownLatch;
    
    public class DateUtil {
        private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        public static String format(Date date) {
            return dateFormat.format(date);
        }
    
        public static Date parse(String dateStr) throws ParseException {
            return dateFormat.parse(dateStr);
        }
    
        public static void main(String[] args) {
            final CountDownLatch latch = new CountDownLatch(1);
            final String[] strs = new String[] {"2016-01-01 10:24:00", "2016-01-02 20:48:00", "2016-01-11 12:24:00"};
            for (int i = 0; i < 10; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            latch.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        for (int i = 0; i < 10; i++){
                            try {
                                System.out.println(Thread.currentThread().getName()+ "	" + parse(strs[i % strs.length]));
                                Thread.sleep(100);
                            } catch (ParseException e) {
                                e.printStackTrace();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }).start();
            }
            latch.countDown();
        }
    }

    看一下运行的结果:

    Thread-9    Fri Jan 01 10:24:00 CST 2016
    Thread-1    Sat Feb 25 00:48:00 CST 20162017
    Thread-5    Sat Feb 25 00:48:00 CST 20162017
    Exception in thread "Thread-4" Exception in thread "Thread-6" java.lang.NumberFormatException: For input string: "2002.E20022E"
        at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
        at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.lang.Double.parseDouble(Double.java:538)
        at java.text.DigitList.getDouble(DigitList.java:169)
        at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
        at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
        at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
        at java.text.DateFormat.parse(DateFormat.java:364)
        at DateUtil.parse(DateUtil.java:24)
        at DateUtil$2.run(DateUtil.java:45)

    那么为什么SimpleDateFormat不是线程安全的呢?

    查找问题:

    首先看一下SimpleDateFormat的源码:

    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);
    
        boolean useDateFormatSymbols = useDateFormatSymbols();
    
        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
                count = compiledPattern[i++] << 16;
                count |= compiledPattern[i++];
            }
    
            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char)count);
                break;
    
            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;
    
            default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
            }
        }
        return toAppendTo;
    }

    可以看到format()方法先将日期存放到一个Calendar对象中,而这个Calendar在SimpleDateFormat中是以成员变量的形式存在的。随后调用subFormat()时会再次用到成员变量Calendar。这就是问题所在。同样,在parse()方法里也会存在相应的问题。
    试想,在多线程环境下,如果两个线程都使用同一个SimpleDateFormat实例,那么就有可能存在其中一个线程修改了calendar后紧接着另一个线程也修改了calendar,那么随后第一个线程用到calendar时已经不是它所期待的值了。

    避免问题:

    那么,如何保证SimpleDateFormat的线程安全呢?
    1.每次使用SimpleDateFormat时都创建一个局部的SimpleDateFormat对象,跟一开始的那个方法一样,但是存在性能上的问题,开销较大。
    2.加锁或者同步

    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class DateSyncUtil {
    
        private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
          
        public static String formatDate(Date date)throws ParseException{
            synchronized(sdf){
                return sdf.format(date);
            }  
        }
        
        public static Date parse(String strDate) throws ParseException{
            synchronized(sdf){
                return sdf.parse(strDate);
            }
        } 
    }

    当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。
    3.使用ThreadLocal

    public class DateUtil {
        private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            }
        };
    
        public static String format(Date date) {
            return local.get().format(date);
        }
    
        public static Date parse(String dateStr) throws ParseException {
            return local.get().parse(dateStr);
        }
    }

    使用ThreadLocal可以确保每个线程都可以得到一个单独的SimpleDateFormat对象,既避免了频繁创建对象,也避免了多线程的竞争。

  • 相关阅读:
    Springboot学习:核心配置文件
    Springboot学习:底层依赖与自动配置的原理
    Springboot学习:介绍与HelloWorld
    js根据时间戳倒计时
    windows phone 豆瓣api的封装
    Android开发初始
    PHP(一)
    程序员修炼之道(一)
    WebClient和HttpReuqest两种网络请求的方式
    黑客与画家(二)
  • 原文地址:https://www.cnblogs.com/lyy-2016/p/8638553.html
Copyright © 2020-2023  润新知