java的SimpleDateFormat线程不安全的几种解决方案

 更新时间:2021年08月09日 16:31:16   作者:小虚竹  
但我们知道SimpleDateFormat是线程不安全的,处理时要特别小心,要加锁或者不能定义为static,要在方法内new出对象,再进行格式化,本文就介绍了几种方法,感兴趣的可以了解一下

场景

在java8以前,要格式化日期时间,就需要用到SimpleDateFormat

但我们知道SimpleDateFormat是线程不安全的,处理时要特别小心,要加锁或者不能定义为static,要在方法内new出对象,再进行格式化。很麻烦,而且重复地new出对象,也加大了内存开销。

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

来看看SimpleDateFormat的源码,先看format方法:

// Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);
		...
    }

问题就出在成员变量calendar,如果在使用SimpleDateFormat时,用static定义,那SimpleDateFormat变成了共享变量。那SimpleDateFormat中的calendar就可以被多个线程访问到。

SimpleDateFormat的parse方法也是线程不安全的:

 public Date parse(String text, ParsePosition pos)
    {
     ...
         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;  
 }

由源码可知,最后是调用**parsedDate = calb.establish(calendar).getTime();**获取返回值。方法的参数是calendar,calendar可以被多个线程访问到,存在线程不安全问题。

我们再来看看**calb.establish(calendar)**的源码

image-20210805827464

calb.establish(calendar)方法先后调用了cal.clear()cal.set(),先清理值,再设值。但是这两个操作并不是原子性的,也没有线程安全机制来保证,导致多线程并发时,可能会引起cal的值出现问题了。

验证SimpleDateFormat线程不安全

public class SimpleDateFormatDemoTest {

	private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
    		//1、创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        //2、为线程池分配任务
        ThreadPoolTest threadPoolTest = new ThreadPoolTest();
        for (int i = 0; i < 10; i++) {
            pool.submit(threadPoolTest);
        }
        //3、关闭线程池
        pool.shutdown();
    }


    static class  ThreadPoolTest implements Runnable{

        @Override
        public void run() {
				String dateString = simpleDateFormat.format(new Date());
				try {
					Date parseDate = simpleDateFormat.parse(dateString);
					String dateString2 = simpleDateFormat.format(parseDate);
					System.out.println(Thread.currentThread().getName()+" 线程是否安全: "+dateString.equals(dateString2));
				} catch (Exception e) {
					System.out.println(Thread.currentThread().getName()+" 格式化失败 ");
				}
        }
    }
}

image-20210805754416

出现了两次false,说明线程是不安全的。而且还抛异常,这个就严重了。

解决方案

解决方案1:不要定义为static变量,使用局部变量

就是要使用SimpleDateFormat对象进行format或parse时,再定义为局部变量。就能保证线程安全。

public class SimpleDateFormatDemoTest1 {

    public static void main(String[] args) {
    		//1、创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        //2、为线程池分配任务
        ThreadPoolTest threadPoolTest = new ThreadPoolTest();
        for (int i = 0; i < 10; i++) {
            pool.submit(threadPoolTest);
        }
        //3、关闭线程池
        pool.shutdown();
    }


    static class  ThreadPoolTest implements Runnable{

		@Override
		public void run() {
			SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			String dateString = simpleDateFormat.format(new Date());
			try {
				Date parseDate = simpleDateFormat.parse(dateString);
				String dateString2 = simpleDateFormat.format(parseDate);
				System.out.println(Thread.currentThread().getName()+" 线程是否安全: "+dateString.equals(dateString2));
			} catch (Exception e) {
				System.out.println(Thread.currentThread().getName()+" 格式化失败 ");
			}
		}
    }
}

image-20210805936439

由图可知,已经保证了线程安全,但这种方案不建议在高并发场景下使用,因为会创建大量的SimpleDateFormat对象,影响性能。

解决方案2:加锁:synchronized锁和Lock锁 加synchronized锁

SimpleDateFormat对象还是定义为全局变量,然后需要调用SimpleDateFormat进行格式化时间时,再用synchronized保证线程安全。

public class SimpleDateFormatDemoTest2 {

	private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
    		//1、创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        //2、为线程池分配任务
        ThreadPoolTest threadPoolTest = new ThreadPoolTest();
        for (int i = 0; i < 10; i++) {
            pool.submit(threadPoolTest);
        }
        //3、关闭线程池
        pool.shutdown();
    }

    static class  ThreadPoolTest implements Runnable{

		@Override
		public void run() {
			try {
				synchronized (simpleDateFormat){
					String dateString = simpleDateFormat.format(new Date());
					Date parseDate = simpleDateFormat.parse(dateString);
					String dateString2 = simpleDateFormat.format(parseDate);
					System.out.println(Thread.currentThread().getName()+" 线程是否安全: "+dateString.equals(dateString2));
				}
			} catch (Exception e) {
				System.out.println(Thread.currentThread().getName()+" 格式化失败 ");
			}
		}
    }
}

image-2021080591591

如图所示,线程是安全的。定义了全局变量SimpleDateFormat,减少了创建大量SimpleDateFormat对象的损耗。但是使用synchronized锁,
同一时刻只有一个线程能执行锁住的代码块,在高并发的情况下会影响性能。但这种方案不建议在高并发场景下使用

加Lock锁

加Lock锁和synchronized锁原理是一样的,都是使用锁机制保证线程的安全。

public class SimpleDateFormatDemoTest3 {

	private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	private static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
    		//1、创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        //2、为线程池分配任务
        ThreadPoolTest threadPoolTest = new ThreadPoolTest();
        for (int i = 0; i < 10; i++) {
            pool.submit(threadPoolTest);
        }
        //3、关闭线程池
        pool.shutdown();
    }

    static class  ThreadPoolTest implements Runnable{

		@Override
		public void run() {
			try {
				lock.lock();
					String dateString = simpleDateFormat.format(new Date());
					Date parseDate = simpleDateFormat.parse(dateString);
					String dateString2 = simpleDateFormat.format(parseDate);
					System.out.println(Thread.currentThread().getName()+" 线程是否安全: "+dateString.equals(dateString2));
			} catch (Exception e) {
				System.out.println(Thread.currentThread().getName()+" 格式化失败 ");
			}finally {
				lock.unlock();
			}
		}
    }
}

image-20210805940496

由结果可知,加Lock锁也能保证线程安全。要注意的是,最后一定要释放锁,代码里在finally里增加了lock.unlock();,保证释放锁。
在高并发的情况下会影响性能。这种方案不建议在高并发场景下使用

解决方案3:使用ThreadLocal方式

使用ThreadLocal保证每一个线程有SimpleDateFormat对象副本。这样就能保证线程的安全。

public class SimpleDateFormatDemoTest4 {

	private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
		@Override
		protected DateFormat initialValue() {
			return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		}
	};
    public static void main(String[] args) {
    		//1、创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        //2、为线程池分配任务
        ThreadPoolTest threadPoolTest = new ThreadPoolTest();
        for (int i = 0; i < 10; i++) {
            pool.submit(threadPoolTest);
        }
        //3、关闭线程池
        pool.shutdown();
    }

    static class  ThreadPoolTest implements Runnable{

		@Override
		public void run() {
			try {
					String dateString = threadLocal.get().format(new Date());
					Date parseDate = threadLocal.get().parse(dateString);
					String dateString2 = threadLocal.get().format(parseDate);
					System.out.println(Thread.currentThread().getName()+" 线程是否安全: "+dateString.equals(dateString2));
			} catch (Exception e) {
				System.out.println(Thread.currentThread().getName()+" 格式化失败 ");
			}finally {
				//避免内存泄漏,使用完threadLocal后要调用remove方法清除数据
				threadLocal.remove();
			}
		}
    }
}

image-202108059729

使用ThreadLocal能保证线程安全,且效率也是挺高的。适合高并发场景使用

解决方案4:使用DateTimeFormatter代替SimpleDateFormat

使用DateTimeFormatter代替SimpleDateFormat(DateTimeFormatter是线程安全的,java 8+支持)
DateTimeFormatter介绍 传送门:万字博文教你搞懂java源码的日期和时间相关用法

public class DateTimeFormatterDemoTest5 {
	private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

	public static void main(String[] args) {
		//1、创建线程池
		ExecutorService pool = Executors.newFixedThreadPool(5);
		//2、为线程池分配任务
		ThreadPoolTest threadPoolTest = new ThreadPoolTest();
		for (int i = 0; i < 10; i++) {
			pool.submit(threadPoolTest);
		}
		//3、关闭线程池
		pool.shutdown();
	}


	static class  ThreadPoolTest implements Runnable{

		@Override
		public void run() {
			try {
				String dateString = dateTimeFormatter.format(LocalDateTime.now());
				TemporalAccessor temporalAccessor = dateTimeFormatter.parse(dateString);
				String dateString2 = dateTimeFormatter.format(temporalAccessor);
				System.out.println(Thread.currentThread().getName()+" 线程是否安全: "+dateString.equals(dateString2));
			} catch (Exception e) {
				e.printStackTrace();
				System.out.println(Thread.currentThread().getName()+" 格式化失败 ");
			}
		}
	}
}

image-2021080591443373

使用DateTimeFormatter能保证线程安全,且效率也是挺高的。适合高并发场景使用

解决方案5:使用FastDateFormat 替换SimpleDateFormat

使用FastDateFormat 替换SimpleDateFormat(FastDateFormat 是线程安全的,Apache Commons Lang包支持,不受限于java版本)

public class DateTimeFormatterDemoTest5 {
	private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

	public static void main(String[] args) {
		//1、创建线程池
		ExecutorService pool = Executors.newFixedThreadPool(5);
		//2、为线程池分配任务
		ThreadPoolTest threadPoolTest = new ThreadPoolTest();
		for (int i = 0; i < 10; i++) {
			pool.submit(threadPoolTest);
		}
		//3、关闭线程池
		pool.shutdown();
	}


	static class  ThreadPoolTest implements Runnable{

		@Override
		public void run() {
			try {
				String dateString = dateTimeFormatter.format(LocalDateTime.now());
				TemporalAccessor temporalAccessor = dateTimeFormatter.parse(dateString);
				String dateString2 = dateTimeFormatter.format(temporalAccessor);
				System.out.println(Thread.currentThread().getName()+" 线程是否安全: "+dateString.equals(dateString2));
			} catch (Exception e) {
				e.printStackTrace();
				System.out.println(Thread.currentThread().getName()+" 格式化失败 ");
			}
		}
	}
}

使用FastDateFormat能保证线程安全,且效率也是挺高的。适合高并发场景使用

FastDateFormat源码分析

 Apache Commons Lang 3.5
//FastDateFormat
@Overridepublic String format(final Date date) {
    return printer.format(date);
}
@Override public String format(final Date date) 
{
    final Calendar c = Calendar.getInstance(timeZone, locale);
    c.setTime(date);
    return applyRulesToString(c);
}

源码中 Calender 是在 format 方法里创建的,肯定不会出现 setTime 的线程安全问题。这样线程安全疑惑解决了。那还有性能问题要考虑?

我们来看下FastDateFormat是怎么获取的

FastDateFormat.getInstance();FastDateFormat.getInstance(CHINESE_DATE_TIME_PATTERN);

看下对应的源码

/**
 * 获得 FastDateFormat实例,使用默认格式和地区 *
 * @return FastDateFormat 
*/
public static FastDateFormat getInstance() {
    return CACHE.getInstance();
}
/**
 * 获得 FastDateFormat 实例,使用默认地区
 * 支持缓存 * 
* @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式
 * @return FastDateFormat 
* @throws IllegalArgumentException 日期格式问题 
*/
public static FastDateFormat getInstance(final String pattern) {
    return CACHE.getInstance(pattern, null, null);
}

这里有用到一个CACHE,看来用了缓存,往下看

private static final FormatCache < FastDateFormat > CACHE = new FormatCache < FastDateFormat > ()
{
    @Override protected FastDateFormat createInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
        return new FastDateFormat(pattern, timeZone, locale);
    }
};
//abstract class FormatCache<F extends Format>

{
    ... private final ConcurrentMap < Tuple, F > cInstanceCache = new ConcurrentHashMap <> (7);
    private static final ConcurrentMap < Tuple, String > C_DATE_TIME_INSTANCE_CACHE = new ConcurrentHashMap <> (7);
    ...
}

image-20210728914309

在getInstance 方法中加了ConcurrentMap 做缓存,提高了性能。且我们知道ConcurrentMap 也是线程安全的。

实践

/**
 * 年月格式 {@link FastDateFormat}:yyyy-MM
 */
public static final FastDateFormat NORM_MONTH_FORMAT = FastDateFormat.getInstance(NORM_MONTH_PATTERN);

image-2021072895013629

//FastDateFormat
public static FastDateFormat getInstance(final String pattern) {
    return CACHE.getInstance(pattern, null, null);
}

image-20210728205104833

image-2021072895259113

如图可证,是使用了ConcurrentMap 做缓存。且key值是格式,时区和locale(语境)三者都相同为相同的key。

结论

这个是阿里巴巴 java开发手册中的规定:

img

1、不要定义为static变量,使用局部变量

2、加锁:synchronized锁和Lock锁

3、使用ThreadLocal方式

4、使用DateTimeFormatter代替SimpleDateFormat(DateTimeFormatter是线程安全的,java 8+支持)

5、使用FastDateFormat 替换SimpleDateFormat(FastDateFormat 是线程安全的,Apache Commons Lang包支持,java8之前推荐此用法)

到此这篇关于java的SimpleDateFormat线程不安全的几种解决方案的文章就介绍到这了,更多相关java SimpleDateFormat线程不安全内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java Semaphore信号量使用分析讲解

    Java Semaphore信号量使用分析讲解

    Semaphore实际上是一种共享锁,因为它允许多个线程并发获取共享的资源,在Semaphore对象创建时必须设置可用令牌的初始数量permits,用于控制并发时同时获取资源权限的线程数量,这篇文章主要介绍了Java中的Semaphore如何使用,需要的朋友可以参考下
    2022-12-12
  • Spring Boot使用Druid和监控配置方法

    Spring Boot使用Druid和监控配置方法

    Druid是Java语言中最好的数据库连接池,并且能够提供强大的监控和扩展功能。下面来说明如何在 Spring Boot 中配置使用Druid
    2017-04-04
  • 在Java中使用日志框架log4j的方法

    在Java中使用日志框架log4j的方法

    Log4j有三个主要的组件/对象:Loggers(记录器),Appenders (输出源)和Layouts(布局)。这里可简单理解为日志类别,日志要输出的地方和日志以何种形式输出,今天通过本文给大家分享Java日志框架log4j的相关知识,感兴趣的朋友一起看看吧
    2021-08-08
  • java自定义动态链接数据库示例

    java自定义动态链接数据库示例

    这篇文章主要介绍了java自定义动态链接数据库示例,需要的朋友可以参考下
    2014-02-02
  • Flyway详解及Springboot集成Flyway的详细教程

    Flyway详解及Springboot集成Flyway的详细教程

    Flayway是一款数据库版本控制管理工具,,支持数据库版本自动升级,Migrations可以写成sql脚本,也可以写在java代码里。这篇文章主要介绍了Flyway详解及Springboot集成Flyway的详细教程的相关资料,需要的朋友可以参考下
    2020-07-07
  • zuul集成Sentinel,完成对path映射的限流操作

    zuul集成Sentinel,完成对path映射的限流操作

    这篇文章主要介绍了zuul集成Sentinel,完成对path映射的限流操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • Java StringUtils字符串分割转数组的实现

    Java StringUtils字符串分割转数组的实现

    这篇文章主要介绍了Java StringUtils字符串分割转数组的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • C#中NAudio音频库的安装与使用教程详解

    C#中NAudio音频库的安装与使用教程详解

    NAudio为.NET平台下的开源库,采用ML-PL协议,它支持许多音频操作,可实现多种API播放与录制、多种不同音频格式等功能,下面就跟随小编一起学习一下NAudio的安装与使用吧
    2023-11-11
  • Java的Spring框架下的AOP编程模式示例

    Java的Spring框架下的AOP编程模式示例

    这篇文章主要介绍了Java的Spring框架下的AOP编程模式示例,文中分别讲到了基于XML和基于@AspectJ的自定义方式,需要的朋友可以参考下
    2015-12-12
  • Java 中普通代码块,构造代码块,静态代码块区别及代码示例

    Java 中普通代码块,构造代码块,静态代码块区别及代码示例

    这篇文章主要介绍了Java 中普通代码块,构造代码块,静态代码块区别及代码示例的相关资料,需要的朋友可以参考下
    2017-01-01

最新评论