zoukankan      html  css  js  c++  java
  • SimpleDateFormat非线程安全

    原文地址:

    http://lilongfei1030.blog.163.com/blog/static/860152820136260822266/

    SimpleDateFormat类的继承关系:

    java.text 

    Class SimpleDateFormat

    java.lang.Object

     |

       +----java.text.Format

     |

       +----java.text.DateFormat

    |

       +----java.text.SimpleDateFormat

    源文档 <http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>

    该类用来对日期字符串进行解析和格式化输出.

    SimpleDateFormatjavadoc中有这么句话:

    Synchronization

    Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

    源文档 <http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>

    翻译一下:

    *日期格式是不同步的.

    * 建议为每个线程创建独立的格式实例.

    * 如果多线程并发访问同一个格式,则必须保持外部同步.

    简而言之,SimpleDateFormat不是线程安全的,所以在多线程的环境下,往往会出现意想不到的结果.每次使用时,都创建一个新的SimpleDateFormat实例,或做加锁来同步使用.

    SimpleDateFormat相关问题

    (1)性能问题,主要是创建一个 SimpleDateFormat实例的开销比较昂贵,解析字符串时间时频繁创建生命周期短暂的实例导致性能低下。

    代码如下:

    public class DateUtil {

       

            private static final String FORMAT_YYYMMDD = "yyyy-MM-dd";

        public static String newFormatDate(Date date) {

            return new SimpleDateFormat(FORMAT_YYYMMDD).format(date);

        }

        public static Date newParse(String strDate) throws ParseException {

            return new SimpleDateFormat(FORMAT_YYYMMDD).parse(strDate);

        }

    }

    (2)并发非线程安全问题,即使将 SimpleDateFormat定义为静态类变量,貌似能解决这个问题,但是SimpleDateFormat是非线程安全的,可能引发并发非线程安全问题。

    代码如下:

    public class DateUtil {

         

          private static final SimpleDateFormat SIMPLE_DATE_FORMAT_OBJECT = new SimpleDateFormat("yyyy-MM-dd");

        public static String formatDate(Date date) {

            return SIMPLE_DATE_FORMAT_OBJECT.format(date);

        }

        public static Date parse(String strDate) throws ParseException {

            return SIMPLE_DATE_FORMAT_OBJECT.parse(strDate);

        }

    }

    测试类

    import java.util.Date;

    import java.util.concurrent.ExecutorService;

    import java.util.concurrent.Executors;

    import java.util.concurrent.Semaphore;

    import junit.framework.TestCase;

    import org.apache.log4j.Logger;

    import org.junit.Test;

    /**

     *

     *@Title:SimpleDateFormatTest

     *@Description:SimpleDateFormat类测试

     *@Author:lilongfei

     *@Since:2013-7-20

     *@Version:1.1.0

     */

    public class SimpleDateFormatTest extends TestCase {

        // 线程数

        private static final int THREAD_NUM = 50;

        // 客户端数

        private static final int CLIENT_NUM = 100;

        private static int failCount = 0;

        private static final Logger loger = getDefaultLogger();

        @Override

        public void setUp() throws Exception {

            // TODO: 实现测试前的初始化工作

        }

        @Override

        public void tearDown() throws Exception {

            // 实现测试完成后的垃圾回收、测试结果统计等工作

            loger.info("访问数:" + CLIENT_NUM);

            loger.info("并发数:" + THREAD_NUM);

            loger.info("断言失败数:" + failCount);

        }

        @Test

        public void test() {

            // 得到一个可复用线程的线程池

            ExecutorService exec = Executors.newCachedThreadPool();

            // 信号量

            final Semaphore semp = new Semaphore(THREAD_NUM);

            for (int index = 0; index < CLIENT_NUM; index++) {

                final int no = index;

                Runnable run = new Runnable() {

                    public void run() {

                        // 获取一个准入许可

                        try {

                            semp.acquire();

                            // doFormatTest(no);

                            doParseTest();

                            // 释放一个许可

                            semp.release();

                        } catch (Throwable e) {

                            e.printStackTrace();

                        }

                    }

                };

                // 在线程池中执行一个任务

                exec.execute(run);

            }

            // 退出线程池

            exec.shutdown();

        }

       

        /**

         *

         *

         * @Description:

         */

        private void doParseTest() {

            try {

                DateUtil.parse("2013-07-25");

            } catch (Throwable e) {

                failCount++;

                e.printStackTrace();

            }

        }

       

        /**

         *

         * @param no

         * @Description:

               *    测试时需要修改一下日期,如:当前日期为2013-07-25,明天为2013-07-26

         */

        private void doFormatTest(int no) {

            try {

                if (no % 2 == 0) {

                    String today = DateUtil.formatDate(new Date());

                    assertTrue("ERROR TODAY IS:" + today, "2013-07-25".equals(today));

                } else {

                    String tomorrow = DateUtil.formatDate(new Date(new Date().getTime() + 1000 * 60 * 60 * 24));

                    assertTrue("ERROR TOMORROW IS:" + tomorrow, "2013-07-26".equals(tomorrow));

                }

            } catch (Throwable e) {

                loger.error(e);

                failCount++;

            }

        }

        private static Logger getDefaultLogger() {

            return Logger.getLogger("Businesslog");

        }

    测试用例中使用到log4j,需要在工程根目录下,新建log4j.properties文件

    其中具体内容为

    log4j.rootLogger=DEBUG

    #将逻辑层log记录到BusinessLog,allLog中

    log4j.logger.Businesslog=DEBUG,A1

    #A1--打印到屏幕上

    log4j.appender.A1=org.apache.log4j.ConsoleAppender

    log4j.appender.A1.layout=org.apache.log4j.PatternLayout

    log4j.appender.A1.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%m%n

    引入第三方Jar包,log4j-1.2.15.jarjunit-4.5.jar

    执行结果为:

    java.lang.NumberFormatException: multiple points

    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1084)

    at java.lang.Double.parseDouble(Double.java:482)

    at java.text.DigitList.getDouble(DigitList.java:141)

    at java.text.DecimalFormat.parse(DecimalFormat.java:1276)

    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1375)

    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1124)

    at java.text.DateFormat.parse(DateFormat.java:333)

    at com.DateUtil.parse(DateUtil.java:20)

    at com.SimpleDateFormatTest.doParseTest(SimpleDateFormatTest.java:143)

    at com.SimpleDateFormatTest.access$0(SimpleDateFormatTest.java:141)

    at com.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:71)

    at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:650)

    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:675)

    at java.lang.Thread.run(Thread.java:595)

    java.lang.NumberFormatException: For input string: ""

    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)

    at java.lang.Long.parseLong(Long.java:424)

    at java.lang.Long.parseLong(Long.java:461)

    at java.text.DigitList.getLong(DigitList.java:167)

    at java.text.DecimalFormat.parse(DecimalFormat.java:1271)

    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1692)

    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1124)

    at java.text.DateFormat.parse(DateFormat.java:333)

    at com.DateUtil.parse(DateUtil.java:20)

    at com.SimpleDateFormatTest.doParseTest(SimpleDateFormatTest.java:143)

    at com.SimpleDateFormatTest.access$0(SimpleDateFormatTest.java:141)

    at com.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:71)

    at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:650)

    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:675)

    at java.lang.Thread.run(Thread.java:595)

    如果用 synchronized 线程同步同样面临性能上的问题,同步将导致性能下降(线程之间序列化的获取SimpleDateFormat实例)。

    使用Threadlocal解决此问题

    以下转载至:源文档 <http://www.oschina.net/question/12_45856> 未做验证。

    对于每个线程SimpleDateFormat不存在影响他们之间协作的状态,为每个线程创建一个SimpleDateFormat变量的拷贝或者叫做副本,代码如下:

    import java.text.DateFormat;

    import java.text.ParseException;

    import java.text.SimpleDateFormat;

    import java.util.Date;

    /**

    * 使用ThreadLocal以空间换时间解决SimpleDateFormat线程安全问题。

    *

    * @author

    *

    */

    public class DateUtil {

    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

    @SuppressWarnings("rawtypes")

    private static ThreadLocal threadLocal = new ThreadLocal() {

    protected synchronized Object initialValue() {

    return new SimpleDateFormat(DATE_FORMAT);

    }

    };

    public static DateFormat getDateFormat() {

    return (DateFormat) threadLocal.get();

    }

    public static Date parse(String textDate) throws ParseException {

    return getDateFormat().parse(textDate);

    }

    }

    创建一个ThreadLocal类变量,这里创建时用了一个匿名类,覆盖了initialValue方法,主要作用是创建时初始化实例。也可以采用下面方式创建;

    import java.text.DateFormat;

    import java.text.SimpleDateFormat;

    /**

    * 使用ThreadLocal以空间换时间解决SimpleDateFormat线程安全问题。

    *

    * @author

    *

    */

    public class DateUtil {

    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

    // 第一次调用get将返回null

    private static ThreadLocal threadLocal = new ThreadLocal();

    // 获取线程的变量副本,如果不覆盖initialValue,第一次get返回null,故需要初始化一个SimpleDateFormat,并set到threadLocal中

    public static DateFormat getDateFormat() {

    DateFormat df = (DateFormat) threadLocal.get();

    if (df == null) {

    df = new SimpleDateFormat(DATE_FORMAT);

    threadLocal.set(df);

    }

    return df;

    }

    }

    我们看下我们覆盖的initialValue方法:

    protected T initialValue() {

    return null;//直接返回null

    }

    关于ThreadLocal相关知识可参考:源文档<http://blog.sina.com.cn/s/blog_871746680100yuir.html>

       总之,使用 SimpleDateFormat 应该关注以下几点:

    • 确保不会在多线程状态下使用同一个 DateFormat 或者 SimpleDateFormat 实例
    • 如果多线程情况下需要访问同一个实例,那么请用同步方法
    • 可以使用 JODA 日期时间处理库来避免这些问题
    • 可以使用 commons-lang 包中的 FastDateFormat 工具类

    Commons项目中用来处理Java基本对象方法的工具类包,可以简化很多平时经常要用到的写法,例如判断字符串是否为空等等。

    • 可以使用 ThreadLocal 来处理这个问题
  • 相关阅读:
    这是阿里技术专家对 SRE 和稳定性保障的理解
    阿里四年技术 TL 的得失总结:如何做好技术 Team Leader
    深度 | 阿里云蒋江伟:什么是真正的云原生?
    亲历者说 | 完整记录一年多考拉海购的云原生之路
    Seata RPC 模块的重构之路
    对容器镜像的思考和讨论
    20 行代码:Serverless 架构下用 Python 轻松搞定图像分类和预测
    怎么提升写代码的能力
    云原生 DevOps 的 5 步升级路径
    dubbo-go 白话文 | 从零搭建 dubbogo 和 dubbo 的简单用例
  • 原文地址:https://www.cnblogs.com/alexlo/p/4027168.html
Copyright © 2011-2022 走看看