• SimpleDateFormat线程不安全及解决的方法


    一. 为什么SimpleDateFormat不是线程安全的?

    Java源代码例如以下:

    /**
    * 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.
    */
    public class SimpleDateFormat extends DateFormat {
    	
    	public Date parse(String text, ParsePosition pos){
    		calendar.clear(); // Clears all the time fields
    		// other logic ...
    		Date parsedDate = calendar.getTime();
    	}
    }
    
    
    abstract class DateFormat{
    	// other logic ...
    	protected Calendar calendar;
    	public Date parse(String source) throws ParseException{
            ParsePosition pos = new ParsePosition(0);
            Date result = parse(source, pos);
            if (pos.index == 0)
                throw new ParseException("Unparseable date: "" + source + """ ,
                    pos.errorIndex);
            return result;
        }
    }
    假设我们把SimpleDateFormat定义成static成员变量,那么多个thread之间会共享这个sdf对象, 所以Calendar对象也会共享。

    假定线程A和线程B都进入了parse(text, pos) 方法。 线程B运行到calendar.clear()后,线程A运行到calendar.getTime(), 那么就会有问题。


    二. 问题重现:

    public class DateFormatTest {
    	private static SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
    	private static String date[] = { "01-Jan-1999", "01-Jan-2000", "01-Jan-2001" };
    
    	public static void main(String[] args) {
    		for (int i = 0; i < date.length; i++) {
    			final int temp = i;
    			new Thread(new Runnable() {
    				@Override
    				public void run() {
    					try {
    						while (true) {
    							String str1 = date[temp];
    							String str2 = sdf.format(sdf.parse(str1));
    							System.out.println(Thread.currentThread().getName() + ", " + str1 + "," + str2);
    							if(!str1.equals(str2)){
    								throw new RuntimeException(Thread.currentThread().getName() 
    										+ ", Expected " + str1 + " but got " + str2);
    							}
    						}
    					} catch (Exception e) {
    						throw new RuntimeException("parse failed", e);
    					}
    				}
    			}).start();
    		}
    	}
    }

    创建三个进程, 使用静态成员变量SimpleDateFormat的parse和format方法。然后比較经过这两个方法折腾后的值是否相等:

    程序假设出现下面错误,说明传入的日期就串掉了,即SimpleDateFormat不是线程安全的:

    Exception in thread "Thread-0" java.lang.RuntimeException: parse failed
    	at cn.test.DateFormatTest$1.run(DateFormatTest.java:27)
    	at java.lang.Thread.run(Thread.java:662)
    Caused by: java.lang.RuntimeException: Thread-0, Expected 01-Jan-1999 but got 01-Jan-2000
    	at cn.test.DateFormatTest$1.run(DateFormatTest.java:22)
    	... 1 more

    三. 解决方式:

    1. 解决方式a:

    将SimpleDateFormat定义成局部变量:

    SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
    String str1 = "01-Jan-2010";
    String str2 = sdf.format(sdf.parse(str1));
    缺点:每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。


    2. 解决方式b:

    加一把线程同步锁:synchronized(lock)

    public class SyncDateFormatTest {
    	private static SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
    	private static String date[] = { "01-Jan-1999", "01-Jan-2000", "01-Jan-2001" };
    
    	public static void main(String[] args) {
    		for (int i = 0; i < date.length; i++) {
    			final int temp = i;
    			new Thread(new Runnable() {
    				@Override
    				public void run() {
    					try {
    						while (true) {
    							synchronized (sdf) {
    								String str1 = date[temp];
    								Date date = sdf.parse(str1);
    								String str2 = sdf.format(date);
    								System.out.println(Thread.currentThread().getName() + ", " + str1 + "," + str2);
    								if(!str1.equals(str2)){
    									throw new RuntimeException(Thread.currentThread().getName() 
    											+ ", Expected " + str1 + " but got " + str2);
    								}
    							}
    						}
    					} catch (Exception e) {
    						throw new RuntimeException("parse failed", e);
    					}
    				}
    			}).start();
    		}
    	}
    }
    缺点:性能较差,每次都要等待锁释放后其它线程才干进入


    3. 解决方式c: (推荐)

    使用ThreadLocal: 每一个线程都将拥有自己的SimpleDateFormat对象副本。

    写一个工具类:

    public class DateUtil {
    	private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>();
    
    	public static Date parse(String str) throws Exception {
    		SimpleDateFormat sdf = local.get();
    		if (sdf == null) {
    			sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
    			local.set(sdf);
    		}
    		return sdf.parse(str);
    	}
    	
    	public static String format(Date date) throws Exception {
    		SimpleDateFormat sdf = local.get();
    		if (sdf == null) {
    			sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
    			local.set(sdf);
    		}
    		return sdf.format(date);
    	}
    }
    測试代码:

    public class ThreadLocalDateFormatTest {
    	private static String date[] = { "01-Jan-1999", "01-Jan-2000", "01-Jan-2001" };
    
    	public static void main(String[] args) {
    		for (int i = 0; i < date.length; i++) {
    			final int temp = i;
    			new Thread(new Runnable() {
    				@Override
    				public void run() {
    					try {
    						while (true) {
    							String str1 = date[temp];
    							Date date = DateUtil.parse(str1);
    							String str2 = DateUtil.format(date);
    							System.out.println(str1 + "," + str2);
    							if(!str1.equals(str2)){
    								throw new RuntimeException(Thread.currentThread().getName() 
    										+ ", Expected " + str1 + " but got " + str2);
    							}
    						}
    					} catch (Exception e) {
    						throw new RuntimeException("parse failed", e);
    					}
    				}
    			}).start();
    		}
    	}
    }






  • 相关阅读:
    20200323 Go语言基础
    20200313 图表工具与redis使用
    20200312 CMDB的磁盘数据查询
    20200311 CMDB的表设计
    20200320 代码发布之完结
    20200319 代码发布之任务发布钩子脚本
    Ubuntu 安装 MySQL 服务
    使用U盘重装系统(删除掉系统自带的Windows 10)
    Linux & Windows 上安装 Qt
    初次使用Tampermonkey脚本管理器
  • 原文地址:https://www.cnblogs.com/yangykaifa/p/7323333.html
Copyright © 2020-2023  润新知