Java并发编程笔记之SimpleDateFormat源码分析

SimpleDateFormat 是 Java 提供的一个格式化和解析日期的工具类,平常开发中应该常常会用到,可是因为它是线程不安全的,多线程公用一个 SimpleDateFormat 实例对日期进行解析或者格式化会致使程序出错,本节就讨论下它为什么是线程不安全的,以及如何避免。java

为了复现上面所说的不安全,咱们要用一个例子来突出这个不安全,例子以下:安全

package com.hjc;

import java.text.ParseException;
import java.text.SimpleDateFormat;

/**
 * Created by cong on 2018/7/12.
 */
public class SimpleDateFormatTest {

    //(1)建立单例实例
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        //(2)建立多个线程,并启动
        for (int i = 0; i <10 ; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {//(3)使用单例日期实例解析文本
                        System.out.println(sdf.parse("2018-07-12 15:18:00"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();//(4)启动线程
        }
    }

}

运行结果以下:多线程

代码(1)建立了 SimpleDateFormat 的一个实例,代码(2)建立 10 个线程,每一个线程都公用同一个 sdf 对象对文本日期进行解析,多运行几回就会抛出 java.lang.NumberFormatException 异常,加大线程的个数有利于该问题复现。ide

 

为何会出现这样的问题呢?工具

那么接下来咱们就要进入到SimpleDateFormat 源码一探究竟,为了便于分析首先查看 SimpleDateFormat 的类图结构,类图以下所示:ui

可知每一个 SimpleDateFormat 实例里面有一个 Calendar 对象,到后面就会知道SimpleDateFormat 之因此是线程不安全的,其实就是由于 Calendar 是线程不安全的,后者之因此是线程不安全的是由于其中存放日期数据的变量都是线程不安全的,好比里面的 fields,time 等。spa

接下来咱们要看看parse方法到底干了些什么事,源码以下:线程

 public Date parse(String text, ParsePosition pos)
    {

        //(1)解析日期字符串放入CalendarBuilder的实例calb中,源码很长,省略一部分,本身去看
        .....

        Date parsedDate;
        try {//(2)使用calb中解析好的日期数据设置calendar
            parsedDate = calb.establish(calendar).getTime();
            ...
        }

        catch (IllegalArgumentException e) {
           ...
            return null;
        }

        return parsedDate;
    }
Calendar establish(Calendar cal) {
   ...
   //(3)重置日期对象cal的属性值
   cal.clear();
   //(4) 使用calb中中属性设置cal
   ...
   //(5)返回设置好的cal对象
   return cal;
}

代码(1)主要的做用是解析字符串日期并把解析好的数据放入了 CalendarBuilder 的实例 calb 中,CalendarBuilder 是一个建造者模式,用来存放后面须要的数据。code

代码(3)重置 Calendar 对象里面的属性值,源码以下:orm

public final void clear(){
       for (int i = 0; i < fields.length; ) {
           stamp[i] = fields[i] = 0; // UNSET == 0
           isSet[i++] = false;
       }
       areAllFieldsSet = areFieldsSet = false;
       isTimeSet = false;
}

代码(4)使用 calb 中解析好的日期数据设置 cal 对象

代码(5) 返回设置好的 cal 对象

 

从上面代码能够知道代码(3)(4)(5)操做不是原子性操做,当多个线程调用 parse 方法时候好比线程 A 执行了代码(3)(4)也就是设置好了 cal 对象,在执行代码(5)前线程 B 执行了代码(3)清空了 cal 对象,因为多个线程使用的是一个 cal 对象,因此线程 A 执行代码(5)返回的就多是被线程 B 清空后的对象,固然也有可能线程 B 执行了代码(4)被线程 B 修改后的 cal 对象。从而致使程序错误。

 

那么,让咱们思考一个问题,如何解决SimpleDateFormat 的线程安全性问题呢?

  1.第一种方式:每次使用时候 new 一个 SimpleDateFormat 的实例,这样能够保证每一个实例使用本身的 Calendar 实例, 可是每次使用都须要 new 一个对象,而且使用后因为没有其它引用,就会须要被回收,开销会很大。

  2.第二种方式:究其缘由是由于多线程下代码(3)(4)(5)三个步骤不是一个原子性操做,那么容易想到的是对其进行同步,让(3)(4)(5)成为原子操做,可使用 synchronized 进行同步,例子改造以下所示:

/**
 * Created by cong on 2018/7/12.
 */
public class SimpleDateFormatTest1 {

    //(1)建立单例实例
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        //(2)建立多个线程,并启动
        for (int i = 0; i <10 ; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {// (3)使用单例日期实例解析文本
                        synchronized (sdf) {
                            System.out.println(sdf.parse("2018-07-12 15:18:00"));
                        }
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();//(4)启动线程
        }
    }

}

运行结果以下:

 

  3.第三种方式:使用 ThreadLocal,这样每一个线程只须要使用一个 SimpleDateFormat 实例相比第一种方式大大节省了对象的建立销毁开销,而且不须要对多个线程直接进行同步,使用 ThreadLocal 方式来保证线程安全,例子以下:

/**
 * Created by cong on 2018/7/12.
 */
public class SimpleDateFormatTest2 {
    // (1)建立threadlocal实例
    static ThreadLocal<DateFormat> safeSdf = new ThreadLocal<DateFormat>(){
        @Override
        protected SimpleDateFormat initialValue(){
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static void main(String[] args) {
        // (2)建立多个线程,并启动
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {// (3)使用单例日期实例解析文本
                        System.out.println(safeSdf.get().parse("2018-07-12 15:18:00"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }finally {
                        //(4)使用完毕记得清除,避免内存泄露
                        safeSdf.remove();
                    }
                }
            });
            thread.start();// (4)启动线程
        }
    }

}

运行结果以下:

代码(1)建立了一个线程安全的 SimpleDateFormat 实例,代码(3)在使用的时候首先使用 get() 方法获取当前线程下 SimpleDateFormat 的实例,在第一次调用 ThreadLocal 的 get()方法适合会触发其 initialValue 方法用来建立当前线程所须要的 SimpleDateFormat 对象。另外须要注意的是代码(4)使用完毕线程变量后要记得进行清理,以免内存泄露。

相关文章
相关标签/搜索