java的SimpleDateFormat线程不安全的几种解决方案
admin
2023-05-01 03:51:07
目录

              场景

              在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)**的源码

              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()+" 格式化失败 ");
              				}
                      }
                  }
              }
              

              出现了两次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()+" 格式化失败 ");
              			}
              		}
                  }
              }
              

              由图可知,已经保证了线程安全,但这种方案不建议在高并发场景下使用,因为会创建大量的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()+" 格式化失败 ");
              			}
              		}
                  }
              }
              


              如图所示,线程是安全的。定义了全局变量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();
              			}
              		}
                  }
              }
              


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

              解决方案3:使用ThreadLocal方式

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

              public class SimpleDateFormatDemoTest4 {
              
              	private static ThreadLocal threadLocal = new ThreadLocal(){
              		@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();
              			}
              		}
                  }
              }
              

              使用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()+" 格式化失败 ");
              			}
              		}
              	}
              }
              


              使用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
              
              {
                  ... private final ConcurrentMap < Tuple, F > cInstanceCache = new ConcurrentHashMap <> (7);
                  private static final ConcurrentMap < Tuple, String > C_DATE_TIME_INSTANCE_CACHE = new ConcurrentHashMap <> (7);
                  ...
              }

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

              实践

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

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

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

              结论

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

              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线程不安全内容请搜索趣讯吧以前的文章或继续浏览下面的相关文章希望大家以后多多支持趣讯吧!

              相关内容

              热门资讯

              苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
              世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
              长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
              应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
              猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
              脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
              demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
              北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
              苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
              应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
              长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
              猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
              阿西吧是什么意思 阿西吧相当于... 即使你没有受到过任何外语培训,你也懂四国语言。汉语:你好英语:Shit韩语:阿西吧(아,씨발! )日...
              脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
              demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
              世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
              北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...