java多线程-SimpleDateFormat不安全(九)

java多线程-SimpleDateFormat不安全(九)

在平常的工作中,我们经常会用到SimpleDateFormat这个类来做日期转换,但是要注意它在多线程下面是不安全的。
simpleDateFormat = new SimpleDateFormat(“yyyy-MM-dd”);

不安全代码测试:


package cn.thread.first.unsafe;


import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

class SimpleDome extends Thread {

    private SimpleDateFormat simpleDateFormat;

    private String str;

    public SimpleDome(SimpleDateFormat sf, String str) {
        this.simpleDateFormat = sf;
        this.str = str;
    }

    @Override
    public void run() {
        try {
            Date date = simpleDateFormat.parse(str);
            System.out.println(str + "=" + date);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }


}


public class SimpleDateFormatDome {

    public static void main(String args[]) {


        //第一种:输出结果对不上
      SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

        String[] strs = {"2017-01-10", "2017-01-11", "2017-01-12", "2017-01-13", "2017-01-14", "2017-01-15"};

        SimpleDome[] domes = new SimpleDome[6];
        for (int i = 0; i < 6; i++) {
            domes[i] = new SimpleDome(simpleDateFormat, strs[i]);
        }

        for (int i = 0; i < 6; i++) {
            domes[i].start();
        } 
    }

    //第二种:报各种异常
    final SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy-MM-dd");
        new Thread(new Runnable() {
            public void run() {
                String str = "2017-01-10";
                Date date = new SimpleDome(simpleDateFormat2, str).stToDate();
                System.out.println(str + "=" + date);
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                String str = "2017-01-11";
                Date date = new SimpleDome(simpleDateFormat2,str).stToDate();
                System.out.println(str + "=" + date);
            }
        }).start();
}

第一种输出结果:
异常一:Exception in thread “Thread-3” java.lang.NumberFormatException: For input string: “20172017E4”
异常二:Exception in thread “Thread-2” java.lang.NumberFormatException: multiple points
异常三:转换后的日期有时能对上,有时不能对上。

//第二种:报各种异常
异常一:Exception in thread “Thread-3” java.lang.NumberFormatException: For input string: “20172017E4”
异常二:Exception in thread “Thread-2” java.lang.NumberFormatException: multiple points
异常三:Exception in thread “Thread-3” java.lang.NumberFormatException: For input string: “”

解决方案一

1.在转换的过程中加入锁:

 private static final Object object=newe Object();

 @Override
    public void run() {
        synchronized(object){
            try {
                Date date = simpleDateFormat.parse(str);
                System.out.println(str + "=" + date);
            } catch (ParseException e) {
                e.printStackTrace();
            }

        }
     }

这种方式在高并发下面,会导致大量线程阻塞,严重影响性能,一般不建议这样使用。

解决方案二:

class SimpleDome 
...  
 private SimpleDateFormat simpleDateFormat= new   SimpleDateFormat("yyyy-MM-dd");

对于每个线程来说都是一个新的simpleDateFormat对象,不存在资源竞争。这种方式的缺陷是,同样访问量大时,会创建大量的simpleDateFormat对象,并且转换后就丢去,短暂的占用内存,导致垃圾回收时也要费一番功夫。耗时,耗空间,更不划算。

现在比较流行的解决方案三:

package cn.thread.first.unsafe;


import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

class SimpleHelp {

    private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };

    //写法一,和上面的初始化一起哦!
    public static Date convert(String source) {
        try {
            Thread.sleep(100);
            return df.get().parse(source);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    //写法二
//    public static DateFormat getDateFormat() {
//        DateFormat dateFormat = df.get();
//        if (dateFormat == null) {
//            dateFormat = new SimpleDateFormat("yyyy-MM-dd");
//            df.set(dateFormat);
//        }
//        return dateFormat;
//    }
//
//    public static Date convert2(String source) {
//        try {
//            Thread.sleep(100);
//            return getDateFormat().parse(source);
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
//        return null;
//    }

}

public class SimpleDateFormatDome {

    public static void main(String args[]) {

        //下面是线程安全的
        new Thread(new Runnable() {
            public void run() {
                String str = "2017-01-12";
                Date date = SimpleHelp.convert(str);
                System.out.println(str + "=" + date);
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                String str = "2017-01-13";
                Date date = SimpleHelp.convert(str);
                System.out.println(str + "=" + date);
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                String str = "2017-01-14";
                Date date = SimpleHelp.convert(str);
                System.out.println(str + "=" + date);
            }
        }).start();

    }
}

正常输出结果。
说明:使用ThreadLocal对与每个线程来说都会存一个副本出来,每个线程拥有自己的ThreadLocal-> DateFormat,等于每个线程各自拥有各子的DateFormat。

ThreadLocal在《java多线程-线程间通讯(七)》中也有讲过。这里再讲一下,说明一下网上很多人认为ThreadLocal有多牛,其实也没你想象中的牛。

package cn.thread.first.threadlocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;



class ClassDome {

}

class UserTask2 implements Runnable {
    private static ThreadLocal<ClassDome> startDate = new ThreadLocal<ClassDome>();

    public ClassDome getClassDome() {
        ClassDome dome = startDate.get();
        if (dome == null) {
            System.out.println("我被实例化了");
            dome = new ClassDome();
            startDate.set(dome);
        }
        return dome;
    }

    public void run() {

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ClassDome classDome = new ClassDome();
        System.out.println("localEnd+" + Thread.currentThread().getId() + "==" + getClassDome().getClass().hashCode());
        System.out.println("1111+" + Thread.currentThread().getId() + "==" + classDome.hashCode());
        startDate.remove();//最好显示情况ThreadLocal,不然容易导致内存溢出
    }
}


public class ThreadLocalDemo2 {
    public static void main(String[] args) {
        UserTask2 task = new UserTask2();
        ExecutorService ex = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            ex.execute(task);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        ex.shutdown();
    }
}

输出结果:
我被实例化了
localEnd+10==1443517032
1111+10==1489876697
我被实例化了
localEnd+11==1443517032
1111+11==1514623189
我被实例化了
localEnd+10==1443517032
1111+10==502084774

看到了吗,三个线程,被实例化了三次,也就是说三个线程,创建了三个new ClassDome();那和上面说的第二种方式都就变成一样的了嘛。
说实话,就是一样的。那为什么还要用ThreadLocal?如果一个线程里面有多个日期需要转换时,这个时候就拥有只会有一个哦!假如一个线程处理10条数据的格式转换,这样是对于ThreadLocal方式来说,一个线程里面只创建了一个SimpleDateFormat对象,而用第二种方式则会创建10个SimpleDateFormat对象。把日期转换写成一个工具类,不要继承Thread,就知道ThreadLocal的好处了。
如:
Date date=ConcurrentDateUtil.parse(“日期字符串1”);
Date date=ConcurrentDateUtil.parse(“日期字符串2”);
Date date=ConcurrentDateUtil.parse(“日期字符串3”);

SimpleDateFormat不安全的原因?

SimpleDateFormat继承了DateFormat,在DateFormat中定义了一个protected属性的 Calendar类的对象:calendar。只是因为Calendar累的概念复杂,牵扯到时区与本地化等等,Jdk的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误。

字符串日期转换代码:
simpleDateFormat.parse(str);
parse源码如下:


 public Date parse(String text, ParsePosition pos)
    {
        checkNegativeNumberExpression();

        int start = pos.index;
        int oldStart = start;
        int textLength = text.length();

        boolean[] ambiguousYear = {false};

        CalendarBuilder calb = new CalendarBuilder();

        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:
                if (start >= textLength || text.charAt(start) != (char)count) {
                    pos.index = oldStart;
                    pos.errorIndex = start;
                    return null;
                }
                start++;
                break;

            case TAG_QUOTE_CHARS:
                while (count-- > 0) {
                    if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
                        pos.index = oldStart;
                        pos.errorIndex = start;
                        return null;
                    }
                    start++;
                }
                break;

            default:
                boolean obeyCount = false;
                    boolean useFollowingMinusSignAsDelimiter = false;

                if (i < compiledPattern.length) {
                    int nextTag = compiledPattern[i] >>> 8;
                    if (!(nextTag == TAG_QUOTE_ASCII_CHAR ||
                          nextTag == TAG_QUOTE_CHARS)) {
                        obeyCount = true;
                    }

                    if (hasFollowingMinusSign &&
                        (nextTag == TAG_QUOTE_ASCII_CHAR ||
                         nextTag == TAG_QUOTE_CHARS)) {
                        int c;
                        if (nextTag == TAG_QUOTE_ASCII_CHAR) {
                            c = compiledPattern[i] & 0xff;
                        } else {
                            c = compiledPattern[i+1];
                        }

                        if (c == minusSign) {
                            useFollowingMinusSignAsDelimiter = true;
                        }
                    }
                }
                start = subParse(text, start, tag, count, obeyCount,
                                 ambiguousYear, pos,
                                 useFollowingMinusSignAsDelimiter, calb);
                if (start < 0) {
                    pos.index = oldStart;
                    return null;
                }
            }
        }

        // At this point the fields of Calendar have been set.  Calendar
        // will fill in default values for missing fields when the time
        // is computed.

        pos.index = start;

        Date parsedDate;
        try {
            parsedDate = calb.establish(calendar).getTime();
            // If the year value is ambiguous,
            // then the two-digit year == the default start year
            if (ambiguousYear[0]) {
                if (parsedDate.before(defaultCenturyStart)) {
                    parsedDate = calb.addYear(100).establish(calendar).getTime();
                }
            }
        }
        // An IllegalArgumentException will be thrown by Calendar.getTime()
        // if any fields are out of range, e.g., MONTH == 17.
        catch (IllegalArgumentException e) {
            pos.errorIndex = start;
            pos.index = oldStart;
            return null;
        }

        return parsedDate;
    }

calb.establish(calendar)如下:

Calendar establish(Calendar cal) {
        boolean weekDate = isSet(WEEK_YEAR)
                            && field[WEEK_YEAR] > field[YEAR];
        if (weekDate && !cal.isWeekDateSupported()) {
            // Use YEAR instead
            if (!isSet(YEAR)) {
                set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
            }
            weekDate = false;
        }

        cal.clear();
        // Set the fields from the min stamp to the max stamp so that
        // the field resolution works in the Calendar.
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
            for (int index = 0; index <= maxFieldIndex; index++) {
                if (field[index] == stamp) {
                    cal.set(index, field[MAX_FIELD + index]);
                    break;
                }
            }
        }

        if (weekDate) {
            int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
            int dayOfWeek = isSet(DAY_OF_WEEK) ?
                                field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
            if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
                if (dayOfWeek >= 8) {
                    dayOfWeek--;
                    weekOfYear += dayOfWeek / 7;
                    dayOfWeek = (dayOfWeek % 7) + 1;
                } else {
                    while (dayOfWeek <= 0) {
                        dayOfWeek += 7;
                        weekOfYear--;
                    }
                }
                dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
            }
            cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
        }
        return cal;
    }

第一:Calendar是共享成员变量,而establish这个方法里面有一个clear清除操作,然后再对Calendar重新设置。
其中,calendar是DateFormat的protected字段。这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。
在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
线程1调用format方法,改变了calendar这个字段。
中断来了。
线程2开始执行,它也改变了calendar。
又中断了。
线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。
稍微花点时间分析一下format的实现,我们便不难发现,用到calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。

这个问题背后隐藏着一个更为重要的问题–无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。

  这也同时提醒我们在开发和设计系统的时候注意下一下三点:

  1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明
  2.对线程环境下,对每一个共享的可变变量都要注意其线程安全性
  3.我们的类和方法在做设计的时候,要尽量设计成无状态的

参照文章

总结一

1)使用本地DateFormat 或SimpleDateFormat 对象转换或格式化Java中的日期。使他们本地化确保它们不会在多个线程之间共享。

2)如果您在Java中为SimpleDateFormat 类共享Date ,则需要在外部进行同步调用format()和parse()方法,因为它们会改变DateFormat 对象的状态,并且可以创建微妙和难度在Java中格式化字符串或创建日期时修复错误。最好是避免共享DateFormat 类。

3)如果您有选项,请使用joda-date库 进行日期和时间相关操作。它使用Java Date API易于理解和便携,并解决与Java 中的SimpleDateFormat 相关的所有线程安全问题。

4)Java 中SimpleDateFormat的另一个好的替代方法是Apaches的commons.lang 包,它包含一个名为 FastDateFormat的实用类和线程安全替代Java中的SimpleDateFormat 的类。

5)同步DateFormat 和SimpleDateFormat的另一种方法是使用ThreadLocal,它在每个Thread基础上创建SimpleDateFormat ,但如果不仔细使用,它可能是严重内存泄漏的源头和java.lang.OutOfMemoryError,所以避免你没有任何其他选择。

英文原话

总结二

SimpleDateFormat 的不安全来自于使用了一个全局变量Calendar,而这个变量在操作过程中做了clear,set操作,类似-1,+1操作,这样就导致了SimpleDateFormat在多线程下操作是不安全的。

多线程下只有共享了SimpleDateFormat才会出现上面的情况哦!

猜你喜欢

转载自blog.csdn.net/piaoslowly/article/details/81476059