转载整合:SimpleDateFormat日期格式的使用示例 和 JAVA多线程中SimpleDateFormat不安全的解决方案

本文主要是介绍转载整合:SimpleDateFormat日期格式的使用示例 和 JAVA多线程中SimpleDateFormat不安全的解决方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

SimpleDateFormat日期格式的使用示例

SimpleDateFormat构造函数中字符串的格式,以及各部分代表的含义:

 import java.text.SimpleDateFormat;
import java.util.Date;public class test{public static void main(String args[]) {Date newTime = new Date();//设置时间格式SimpleDateFormat sdf1 = new SimpleDateFormat("y-M-d h:m:s a E");SimpleDateFormat sdf2 = new SimpleDateFormat("yy-MM-dd hh:mm:ss a E");SimpleDateFormat sdf3 = new SimpleDateFormat("yyyy-MMM-ddd hhh:mmm:sss a E");SimpleDateFormat sdf4 = new SimpleDateFormat("yyyyy-MMMM-dddd hhhh:mmmm:ssss a E");//获取的时间,是本机的时间String formatDate1 = sdf1.format(newTime);String formatDate2 = sdf2.format(newTime);String formatDate3 = sdf3.format(newTime);String formatDate4 = sdf4.format(newTime);System.out.println(formatDate1);  System.out.println(formatDate2); System.out.println(formatDate3); System.out.println(formatDate4); }
}

运行结果:
在这里插入图片描述

字符串"yyyy-MM-dd hh:mm:ss",其中:

yyyy : 代表年(不去区分大小写) 假设年份为 2017

"y" , "yyy" , "yyyy" 匹配的都是4位完整的年 如 : "2017""yy" 匹配的是年分的后两位 如 : "15"超过4位,会在年份前面加"0"补位 如 "YYYYY"对应"02017"

MM : 代表月(只能使用大写) 假设月份为 9

"M" 对应 "9""MM" 对应 "09""MMM" 对应 "Sep""MMMM" 对应 "Sep"超出3位,仍然对应 "September"

dd : 代表日(只能使用小写) 假设为13号

"d" , "dd" 都对应 "13"超出2位,会在数字前面加"0"补位. 例如 "dddd" 对应 "0013"

hh : 代表时(区分大小写,大写为24进制计时,小写为12进制计时) 假设为15时

"H" , "HH" 都对应 "15" , 超出2位,会在数字前面加"0"补位. 例如 "HHHH" 对应 "0015""h" 对应 "3""hh" 对应 "03" , 超出2位,会在数字前面加"0"补位. 例如 "hhhh" 对应 "0003"

mm : 代表分(只能使用小写) 假设为32分

"m" , "mm" 都对应 "32" ,  超出2位,会在数字前面加"0"补位. 例如 "mmmm" 对应 "0032"

ss : 代表秒(只能使用小写) 假设为15秒

"s" , "ss" 都对应 "15" , 超出2位,会在数字前面加"0"补位. 例如 "ssss" 对应 "0015"

E : 代表星期(只能使用大写) 假设为 Sunday

"E" , "EE" , "EEE" 都对应 "Sun""EEEE" 对应 "Sunday" , 超出4位 , 仍然对应 "Sunday"

a : 代表上午还是下午,如果是上午就对应 “AM” , 如果是下午就对应 “PM”

其中的分隔符"-"可以替换成其他非字母的任意字符(也可以是汉字),例如:

部分修改:
在这里插入图片描述
运行结果:
在这里插入图片描述

JAVA多线程中SimpleDateFormat不安全的解决方案

不安全代码测试:

package cn.thread.first.unsafe;import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;class SimpleDome extends Thread {private SimpleDateFormat simpleDateFormat;private String str;public SimpleDome(SimpleDateFormat sf, String str) {this.simpleDateFormat = sf;this.str = str;}@Overridepublic void run() {try {Date date = simpleDateFormat.parse(str);System.out.println(str + "=" + date);} catch (ParseException e) {e.printStackTrace();}}}public class SimpleDateFormatDome {public static void main(String args[]) {//第一种:输出结果对不上SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");String[] strs = {"2017-01-10", "2017-01-11", "2017-01-12", "2017-01-13", "2017-01-14", "2017-01-15"};SimpleDome[] domes = new SimpleDome[6];for (int i = 0; i < 6; i++) {domes[i] = new SimpleDome(simpleDateFormat, strs[i]);}for (int i = 0; i < 6; i++) {domes[i].start();} }//第二种:报各种异常final SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy-MM-dd");new Thread(new Runnable() {public void run() {String str = "2017-01-10";Date date = new SimpleDome(simpleDateFormat2, str).stToDate();System.out.println(str + "=" + date);}}).start();new Thread(new Runnable() {public void run() {String str = "2017-01-11";Date date = new SimpleDome(simpleDateFormat2,str).stToDate();System.out.println(str + "=" + date);}}).start();

第一种输出结果:
异常一:Exception in thread “Thread-3” java.lang.NumberFormatException: For input string: “20172017E4”
异常二:Exception in thread “Thread-2” java.lang.NumberFormatException: multiple points
异常三:转换后的日期有时能对上,有时不能对上。

第二种:报各种异常
异常一:Exception in thread “Thread-3” java.lang.NumberFormatException: For input string: “20172017E4”
异常二:Exception in thread “Thread-2” java.lang.NumberFormatException: multiple points
异常三:Exception in thread “Thread-3” java.lang.NumberFormatException: For input string: “”

解决方案一

1.在转换的过程中加入锁:

 private static final Object object=newe Object();@Overridepublic void run() {synchronized(object){try {Date date = simpleDateFormat.parse(str);System.out.println(str + "=" + date);} catch (ParseException e) {e.printStackTrace();}}}

这种方式在高并发下面,会导致大量线程阻塞,严重影响性能,一般不建议这样使用。

解决方案二

class SimpleDome 
...  private SimpleDateFormat simpleDateFormat= new   SimpleDateFormat("yyyy-MM-dd");

对于每个线程来说都是一个新的simpleDateFormat对象,不存在资源竞争。这种方式的缺陷是,同样访问量大时,会创建大量的simpleDateFormat对象,并且转换后就丢去,短暂的占用内存,导致垃圾回收时也要费一番功夫。耗时,耗空间,更不划算。

现在比较流行的解决方案三


package cn.thread.first.unsafe;import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;class SimpleHelp {private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};//写法一,和上面的初始化一起哦!public static Date convert(String source) {try {Thread.sleep(100);return df.get().parse(source);} catch (Exception e) {e.printStackTrace();}return null;}//写法二 getDateFormat()和convert2()public static DateFormat getDateFormat() {DateFormat dateFormat = df.get();if (dateFormat == null) {dateFormat = new SimpleDateFormat("yyyy-MM-dd");df.set(dateFormat);}return dateFormat;}public static Date convert2(String source) {try {Thread.sleep(100);return getDateFormat().parse(source);} catch (Exception e) {e.printStackTrace();}return null;}}public class SimpleDateFormatDome {public static void main(String args[]) {//下面是线程安全的new Thread(new Runnable() {public void run() {String str = "2017-01-12";Date date = SimpleHelp.convert(str);System.out.println(str + "=" + date);}}).start();new Thread(new Runnable() {public void run() {String str = "2017-01-13";Date date = SimpleHelp.convert(str);System.out.println(str + "=" + date);}}).start();new Thread(new Runnable() {public void run() {String str = "2017-01-14";Date date = SimpleHelp.convert(str);System.out.println(str + "=" + date);}}).start();}
}

正常输出结果。
说明:使用ThreadLocal对与每个线程来说都会存一个副本出来,每个线程拥有自己的ThreadLocal-> DateFormat,等于每个线程各自拥有各子的DateFormat。

ThreadLocal在《java多线程-线程间通讯(七)》中也有讲过。这里再讲一下,说明一下网上很多人认为ThreadLocal有多牛,其实也没你想象中的牛。

package cn.thread.first.threadlocal;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;class ClassDome {}class UserTask2 implements Runnable {private static ThreadLocal<ClassDome> startDate = new ThreadLocal<ClassDome>();public ClassDome getClassDome() {ClassDome dome = startDate.get();if (dome == null) {System.out.println("我被实例化了");dome = new ClassDome();startDate.set(dome);}return dome;}public void run() {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}ClassDome classDome = new ClassDome();System.out.println("localEnd+" + Thread.currentThread().getId() + "==" + getClassDome().getClass().hashCode());System.out.println("1111+" + Thread.currentThread().getId() + "==" + classDome.hashCode());startDate.remove();//最好显示情况ThreadLocal,不然容易导致内存溢出}
}public class ThreadLocalDemo2 {public static void main(String[] args) {UserTask2 task = new UserTask2();ExecutorService ex = Executors.newCachedThreadPool();for (int i = 0; i < 3; i++) {ex.execute(task);try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}}ex.shutdown();}
}

输出结果:
我被实例化了
localEnd+10==1443517032
1111+10==1489876697
我被实例化了
localEnd+11==1443517032
1111+11==1514623189
我被实例化了
localEnd+10==1443517032
1111+10==502084774

看到了吗,三个线程,被实例化了三次,也就是说三个线程,创建了三个new ClassDome();那和上面说的第二种方式都就变成一样的了嘛。
说实话,就是一样的。那为什么还要用ThreadLocal?如果一个线程里面有多个日期需要转换时,这个时候就拥有只会有一个哦!假如一个线程处理10条数据的格式转换,这样是对于ThreadLocal方式来说,一个线程里面只创建了一个SimpleDateFormat对象,而用第二种方式则会创建10个SimpleDateFormat对象。把日期转换写成一个工具类,不要继承Thread,就知道ThreadLocal的好处了。
如:
Date date=ConcurrentDateUtil.parse(“日期字符串1”);
Date date=ConcurrentDateUtil.parse(“日期字符串2”);
Date date=ConcurrentDateUtil.parse(“日期字符串3”);

SimpleDateFormat不安全的原因?

SimpleDateFormat继承了DateFormat,在DateFormat中定义了一个protected属性的 Calendar类的对象:calendar。只是因为Calendar累的概念复杂,牵扯到时区与本地化等等,Jdk的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误。

字符串日期转换代码:
simpleDateFormat.parse(str);
parse源码如下:

public Date parse(String text, ParsePosition pos){checkNegativeNumberExpression();int start = pos.index;int oldStart = start;int textLength = text.length();boolean[] ambiguousYear = {false};CalendarBuilder calb = new CalendarBuilder();for (int i = 0; i < compiledPattern.length; ) {int tag = compiledPattern[i] >>> 8;int count = compiledPattern[i++] & 0xff;if (count == 255) {count = compiledPattern[i++] << 16;count |= compiledPattern[i++];}switch (tag) {case TAG_QUOTE_ASCII_CHAR:if (start >= textLength || text.charAt(start) != (char)count) {pos.index = oldStart;pos.errorIndex = start;return null;}start++;break;case TAG_QUOTE_CHARS:while (count-- > 0) {if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {pos.index = oldStart;pos.errorIndex = start;return null;}start++;}break;default:boolean obeyCount = false;boolean useFollowingMinusSignAsDelimiter = false;if (i < compiledPattern.length) {int nextTag = compiledPattern[i] >>> 8;if (!(nextTag == TAG_QUOTE_ASCII_CHAR ||nextTag == TAG_QUOTE_CHARS)) {obeyCount = true;}if (hasFollowingMinusSign &&(nextTag == TAG_QUOTE_ASCII_CHAR ||nextTag == TAG_QUOTE_CHARS)) {int c;if (nextTag == TAG_QUOTE_ASCII_CHAR) {c = compiledPattern[i] & 0xff;} else {c = compiledPattern[i+1];}if (c == minusSign) {useFollowingMinusSignAsDelimiter = true;}}}start = subParse(text, start, tag, count, obeyCount,ambiguousYear, pos,useFollowingMinusSignAsDelimiter, calb);if (start < 0) {pos.index = oldStart;return null;}}}// At this point the fields of Calendar have been set.  Calendar// will fill in default values for missing fields when the time// is computed.pos.index = start;Date parsedDate;try {parsedDate = calb.establish(calendar).getTime();// If the year value is ambiguous,// then the two-digit year == the default start yearif (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;}

第一:Calendar是共享成员变量,而establish这个方法里面有一个clear清除操作,然后再对Calendar重新设置。
其中,calendar是DateFormat的protected字段。这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。
在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
线程1调用format方法,改变了calendar这个字段。
中断来了。
线程2开始执行,它也改变了calendar。
又中断了。
线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。
稍微花点时间分析一下format的实现,我们便不难发现,用到calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。

这个问题背后隐藏着一个更为重要的问题–无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。

这也同时提醒我们在开发和设计系统的时候注意下一下三点:

1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明
  2.对线程环境下,对每一个共享的可变变量都要注意其线程安全性
  3.我们的类和方法在做设计的时候,要尽量设计成无状态的

参照文章:深入理解Java:SimpleDateFormat安全的时间格式化

总结一

1)使用本地DateFormat 或SimpleDateFormat 对象转换或格式化Java中的日期。使他们本地化确保它们不会在多个线程之间共享。

2)如果您在Java中为SimpleDateFormat 类共享Date ,则需要在外部进行同步调用format()和parse()方法,因为它们会改变DateFormat 对象的状态,并且可以创建微妙和难度在Java中格式化字符串或创建日期时修复错误。最好是避免共享DateFormat 类。

3)如果您有选项,请使用joda-date库 进行日期和时间相关操作。它使用Java Date API易于理解和便携,并解决与Java 中的SimpleDateFormat 相关的所有线程安全问题。

4)Java 中SimpleDateFormat的另一个好的替代方法是Apaches的commons.lang 包,它包含一个名为 FastDateFormat的实用类和线程安全替代Java中的SimpleDateFormat 的类。

5)同步DateFormat 和SimpleDateFormat的另一种方法是使用ThreadLocal,它在每个Thread基础上创建SimpleDateFormat ,但如果不仔细使用,它可能是严重内存泄漏的源头和java.lang.OutOfMemoryError,所以避免你没有任何其他选择。

总结二

SimpleDateFormat 的不安全来自于使用了一个全局变量Calendar,而这个变量在操作过程中做了clear,set操作,类似-1,+1操作,这样就导致了SimpleDateFormat在多线程下操作是不安全的。

注:多线程下只有共享了SimpleDateFormat才会出现上面的情况哦!

本文转载整合了以下两篇博文:
转自:https://www.cnblogs.com/jyiqing/p/6858224.html
转自:https://blog.csdn.net/piaoslowly/article/details/81476059
引用了:

深入理解Java:SimpleDateFormat安全的时间格式化

这篇关于转载整合:SimpleDateFormat日期格式的使用示例 和 JAVA多线程中SimpleDateFormat不安全的解决方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/908029

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

部署Vue项目到服务器后404错误的原因及解决方案

《部署Vue项目到服务器后404错误的原因及解决方案》文章介绍了Vue项目部署步骤以及404错误的解决方案,部署步骤包括构建项目、上传文件、配置Web服务器、重启Nginx和访问域名,404错误通常是... 目录一、vue项目部署步骤二、404错误原因及解决方案错误场景原因分析解决方案一、Vue项目部署步骤