SimpleDateFormat 的线程安全问题

来源:互联网 发布:利用淘宝漏洞赚钱 编辑:程序博客网 时间:2024/05/17 20:34

转子:http://blog.csdn.net/zq602316498/article/details/40263083


SimpleDateFormat 的线程安全问题


SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的具体类。 它允许格式化 (date -> text)、语法分析 (text -> date)和标准化。

但是 SimpleDateFormat 并不是一个线程安全的类,在多线程并发访问下会出现问题。通过以下代码进行检验,

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class ProveNotSafe {  
  2.     static SimpleDateFormat df = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);  
  3.     static String testdata[] = { "01-Jan-1999""14-Feb-2001""31-Dec-2007" };  
  4.   
  5.     public static void main(String[] args) {  
  6.         Runnable r[] = new Runnable[testdata.length];  
  7.         for (int i = 0; i < r.length; i++) {  
  8.             final int i2 = i;  
  9.             r[i] = new Runnable() {  
  10.                 public void run() {  
  11.                     try {  
  12.                         for (int j = 0; j < 1000; j++) {  
  13.                             String str = testdata[i2];  
  14.                             String str2 = null;  
  15.                             /* synchronized(df) */{  
  16.                                 Date d = df.parse(str);  
  17.                                 str2 = df.format(d);  
  18.                                 System.out.println("i: " + i2 + "\tj: " + j  
  19.                                         + "\tThreadID: "  
  20.                                         + Thread.currentThread().getId()  
  21.                                         + "\tThreadName: "  
  22.                                         + Thread.currentThread().getName()  
  23.                                         + "\t" + str + "\t" + str2);  
  24.                             }  
  25.                             if (!str.equals(str2)) {  
  26.                                 throw new RuntimeException(  
  27.                                         "date conversion failed after " + j  
  28.                                                 + " iterations. Expected "  
  29.                                                 + str + " but got " + str2);  
  30.                             }  
  31.                         }  
  32.                     } catch (ParseException e) {  
  33.                         throw new RuntimeException("parse failed");  
  34.                     }  
  35.                 }  
  36.             };  
  37.             new Thread(r[i]).start();  
  38.         }  
  39.     }  
  40. }  

多次运行,便会出现异常错误:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. java.lang.RuntimeException: date conversion failed after 0 iterations. Expected 01-Jan-1999 but got 28-Jul-2015  

线程访问的情况大致如下图:


SimpleDateFormat 类内部有一个 Calendar 对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交给 Calendar 引用来储存的.这样就会导致一个问题,如果你的 SimpleDateFormat  是个static 的, 那么多个thread 之间就会共享这个SimpleDateFormat  , 同时也是共享这个Calendar引用,那么就出现时间混乱的情况。


解决方法


(1)第一种方法,也是最简单的解决方案。我们可以把static去掉,这样每个新的线程都会有一个自己的sdf实例,从而避免线程安全的问题。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. SimpleDateFormat df = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);  
  2.                                 Date d = df.parse(str);  
  3.                                 str2 = df.format(d);  

此时访问的情况如下:


然而,使用这种方法,在高并发的情况下会大量的new sdf以及销毁sdf,这样是非常耗费资源的。


(2)第二种方法,使用 ThreadLocal。

在并发情况下,网站的请求任务与线程执行情况大概可以理解为如下。


例如Tomcat的线程池的最大Thread数为4, 现在需要执行的任务有1000个(理解为有1000个用户点了你的网站的某个功能),而这1000个任务都会用到我们写的日期函数处理类

        A) 假如说日期函数处理类使用的是new SimpleDateFormat的方法,那么这里就会有1000次sdf的创建和销毁

        B) Java中提供了一种ThreadLocal的解决方案,它的工作方式是,每个线程只会有一个实例,也就是说我们执行完这1000个任务,总共只会实例化4个sdf.

而且,它并不会有多线程的并发问题。因为,单个线程执行任务肯定是顺序的,例如Thread #1负责执行Task #1-#250, 那么他是顺序而执行Task #1-#250,而Thread #2拥有自己的sdf实例,他也是顺序执行任务 Task #251-#500, 以此类推。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. /* 
  2.  */  
  3. package org.bupt.xiaoye.chapter3;  
  4.   
  5. import java.text.ParseException;  
  6.   
  7. import java.text.SimpleDateFormat;  
  8. import java.util.Date;  
  9. import java.util.HashMap;  
  10. import java.util.Map;  
  11.   
  12. public class DateUtil {  
  13.   
  14.     /** 存放不同的日期模板格式的sdf的Map */  
  15.     private static ThreadLocal<Map<String, SimpleDateFormat>> sdfMap = new ThreadLocal<Map<String, SimpleDateFormat>>() {  
  16.         @Override  
  17.         protected Map<String, SimpleDateFormat> initialValue() {  
  18.             System.out.println(Thread.currentThread().getName() + " init pattern: " + Thread.currentThread());  
  19.             return new HashMap<String, SimpleDateFormat>();  
  20.         }  
  21.     };  
  22.   
  23.     /** 
  24.      * 返回一个SimpleDateFormat,每个线程只会new一次pattern对应的sdf 
  25.      *  
  26.      * @param pattern 
  27.      * @return 
  28.      */  
  29.     private static SimpleDateFormat getSdf(final String pattern) {  
  30.         Map<String, SimpleDateFormat> tl = sdfMap.get();  
  31.         SimpleDateFormat sdf = tl.get(pattern);  
  32.         if (sdf == null) {  
  33.             System.out.println(Thread.currentThread().getName()+" put new sdf of pattern " + pattern + " to map");  
  34.             sdf = new SimpleDateFormat(pattern);  
  35.             tl.put(pattern, sdf);  
  36.         }  
  37.         return sdf;  
  38.     }  
  39.   
  40.     /** 
  41.      * 这样每个线程只会有一个SimpleDateFormat 
  42.      *  
  43.      * @param date 
  44.      * @param pattern 
  45.      * @return 
  46.      */  
  47.     public static String format(Date date, String pattern) {  
  48.         return getSdf(pattern).format(date);  
  49.     }  
  50.   
  51.     public static Date parse(String dateStr, String pattern)  
  52.             throws ParseException {  
  53.         return getSdf(pattern).parse(dateStr);  
  54.     }  
  55.   
  56. }  

这里每个线程都有以 Thread 为key的 Map表,而这个表又以pattern 为key,每一个 pattern 都有一个唯一的 SimpleDateFormat 对象。

我们通过下面代码来测试:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. package org.bupt.xiaoye.chapter3;  
  2.   
  3. import java.text.ParseException;  
  4. import java.util.concurrent.ExecutorService;  
  5. import java.util.concurrent.Executors;  
  6. import java.util.concurrent.TimeUnit;  
  7.   
  8. public class Test {  
  9.   
  10.     public static void main(String[] args) {  
  11.         final String patten1 = "yyyy-MM-dd";  
  12.         final String patten2 = "yyyy-MM";  
  13.   
  14.         Thread t1 = new Thread() {  
  15.   
  16.             @Override  
  17.             public void run() {  
  18.                 try {  
  19.                     DateUtil.parse("1992-09-13", patten1);  
  20.                 } catch (ParseException e) {  
  21.                     e.printStackTrace();  
  22.                 }  
  23.             }  
  24.         };  
  25.   
  26.         Thread t2 = new Thread() {  
  27.   
  28.             @Override  
  29.             public void run() {  
  30.                 try {  
  31.                     DateUtil.parse("2000-09", patten2);  
  32.                 } catch (ParseException e) {  
  33.                     e.printStackTrace();  
  34.                 }  
  35.             }  
  36.         };  
  37.   
  38.         Thread t3 = new Thread() {  
  39.   
  40.             @Override  
  41.             public void run() {  
  42.                 try {  
  43.                     DateUtil.parse("1992-09-13", patten1);  
  44.                 } catch (ParseException e) {  
  45.                     e.printStackTrace();  
  46.                 }  
  47.             }  
  48.         };  
  49.   
  50.         Thread t4 = new Thread() {  
  51.   
  52.             @Override  
  53.             public void run() {  
  54.                 try {  
  55.                     DateUtil.parse("2000-09", patten2);  
  56.                 } catch (ParseException e) {  
  57.                     e.printStackTrace();  
  58.                 }  
  59.             }  
  60.         };  
  61.   
  62.         Thread t5 = new Thread() {  
  63.   
  64.             @Override  
  65.             public void run() {  
  66.                 try {  
  67.                     DateUtil.parse("2000-09-13", patten1);  
  68.                 } catch (ParseException e) {  
  69.                     e.printStackTrace();  
  70.                 }  
  71.             }  
  72.         };  
  73.   
  74.         Thread t6 = new Thread() {  
  75.   
  76.             @Override  
  77.             public void run() {  
  78.                 try {  
  79.                     DateUtil.parse("2000-09", patten2);  
  80.                 } catch (ParseException e) {  
  81.                     e.printStackTrace();  
  82.                 }  
  83.             }  
  84.         };  
  85.   
  86.         System.out.println("单线程执行: ");  
  87.         ExecutorService exec = Executors.newFixedThreadPool(1);  
  88.         exec.execute(t1);  
  89.         exec.execute(t2);  
  90.         exec.execute(t3);  
  91.         exec.execute(t4);  
  92.         exec.execute(t5);  
  93.         exec.execute(t6);  
  94.         exec.shutdown();  
  95.   
  96.         sleep(1000);  
  97.   
  98.         System.out.println("双线程执行: ");  
  99.         ExecutorService exec2 = Executors.newFixedThreadPool(2);  
  100.         exec2.execute(t1);  
  101.         exec2.execute(t2);  
  102.         exec2.execute(t3);  
  103.         exec2.execute(t4);  
  104.         exec2.execute(t5);  
  105.         exec2.execute(t6);  
  106.         exec2.shutdown();  
  107.     }  
  108.   
  109.     private static void sleep(long millSec) {  
  110.         try {  
  111.             TimeUnit.MILLISECONDS.sleep(millSec);  
  112.         } catch (InterruptedException e) {  
  113.             e.printStackTrace();  
  114.         }  
  115.     }  
  116. }  

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. 单线程执行:   
  2. pool-1-thread-1 init pattern: Thread[pool-1-thread-1,5,main]  
  3. pool-1-thread-1 put new sdf of pattern yyyy-MM-dd to map  
  4. pool-1-thread-1 put new sdf of pattern yyyy-MM to map  
  5. 双线程执行:   
  6. pool-2-thread-1 init pattern: Thread[pool-2-thread-1,5,main]  
  7. pool-2-thread-1 put new sdf of pattern yyyy-MM-dd to map  
  8. pool-2-thread-2 init pattern: Thread[pool-2-thread-2,5,main]  
  9. pool-2-thread-2 put new sdf of pattern yyyy-MM to map  
  10. pool-2-thread-1 put new sdf of pattern yyyy-MM to map  
  11. pool-2-thread-2 put new sdf of pattern yyyy-MM-dd to map  

从输出我们可以看出:

        1) 1个线程执行这6个任务的时候,这个线程首次使用过的时候会new一个新的sdf,并且以后都一直用这个sdf,而不是每次处理任务都新建一个新的sdf

        2) 2个线程执行6个任务的时候也是同理,但是2个线程的sdf是分开的,每个线程都有自己的"yyyy-MM-dd", "yyyy-MM"的sdf,所以他们不会有线程安全安全问题

试想,如果使用的是new的实现方法,那么不管是用1个线程去执行,还是用2个线程去执行这6个任务,都需要new 6个sdf


(3)第三种方式,使用同步代码块(synchronized)或者使用装饰器设计模式包装下 SimpleDateFormat ,使之变得线程安全。

也就是在另一篇文章中介绍的 实例封闭机制。 

http://blog.csdn.net/zq602316498/article/details/40143437


(4)第四种方式,使用第三方日期处理函数

比如 JODA 来避免这些问题,你也可以使用 commons-lang 包中的 FastDateFormat 工具类。


参考博文:

http://my.oschina.net/leejun2005/blog/152253

http://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html


0 0
原创粉丝点击