SimpleDateFormat 使用说明
在JAVA的江湖世界,虽然日期转换工具类如此纷繁复杂;但是作为初入世事的JAVA程序员,了解到的第一款日期格式化武器毫无疑问是 “SimpleDateFormat”; 毫无疑问的是 SimpleDateFormat 帮助我们解决了太多太多的日期转换问题。在此向它致敬;
也许在某个深夜,你在研读一些JAVA的经典著作,忽然一行文字引入眼帘——“SimpleDateFormat 线程不安全”,你不禁打了个寒颤,my god。或许,你上线了无数个不安全的 “SimpleDateFormat”,然而它并没有对你的生产环境造成任何实质性的影响。如果是这样,我想你是幸运的,因为你的系统的并发能力还不至于重现 “SimpleDateFormat” 的不安全的场景。
然而作为一个有志的JAVA程序员。你必须要避免任何导致程序异常的情况发生。
所以我们要做的是:
第一步:重现 SimpleDateFormat 多线程问题。
第二步: why? SimpleDateFormat 为什么不安全。
第三步:怎么解决这个问题。
ok,现在让我们来开始探讨这个问题。
SimpleDateFormat 多线程问题重现
好的,我们通过线程池构建一个多线程访问环境,来看看SimpleDateFormat究竟会发生那些问题?
代码如下:
package com.java.thread.demo003;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
*
* SimpleDateFormat 线程不安全问题重现
* */
public class Demo003_a {
private static SimpleDateFormat simpleDateFormat
= new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) {
ExecutorService executors = Executors.newCachedThreadPool();
for(int i=0; i< 10; i++){
executors.execute(()->{
try {
simpleDateFormat.parse("2023-02-08");
System.out.println(Thread.currentThread().getName() + " 日期格式化成功");
} catch (ParseException e) {
System.out.println(Thread.currentThread().getName() + " 解析失败");
e.printStackTrace();
System.exit(1);
}catch (NumberFormatException e) {
System.out.println(Thread.currentThread().getName() + " 解析失败");
e.printStackTrace();
System.exit(1);
}
});
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
executors.shutdown();
System.out.println("所有线程格式化日期成功!!");
}
}
运行结果如下所示,当然失败的姿势多种多样。如果你运气好的话,也有可能运行成功。
"D:\Program Files\Java\jdk1.8.0_181\bin\java.exe" "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2020.2.3\lib\idea_rt.jar=49240:D:\Program Files\JetBrains\IntelliJ IDEA 2020.2.3\bin" -Dfile.encoding=UTF-8 -classpath "D:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;D:\study_note\Code_JavaStudy\Java\target\classes;D:\repository\org\antlr\antlr4-runtime\4.10.1\antlr4-runtime-4.10.1.jar;D:\repository\org\openjdk\jol\jol-core\0.14\jol-core-0.14.jar;D:\repository\org\projectlombok\lombok\1.18.0\lombok-1.18.0.jar" com.java.thread.demo003.Demo003_a
pool-1-thread-1 解析失败
pool-1-thread-2 日期格式化成功
pool-1-thread-3 解析失败
pool-1-thread-4 日期格式化成功
pool-1-thread-5 日期格式化成功
pool-1-thread-6 日期格式化成功
pool-1-thread-7 日期格式化成功
pool-1-thread-8 日期格式化成功
pool-1-thread-9 日期格式化成功
pool-1-thread-10 日期格式化成功
java.lang.NumberFormatException: For input string: "2200022333.E2200022333E44"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.java.thread.demo003.Demo003_a.lambda$main$0(Demo003_a.java:24)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: "2200022333.E2200022333E44"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.java.thread.demo003.Demo003_a.lambda$main$0(Demo003_a.java:24)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 1
为什么parse方法解析的对象不是合法的日期字符串类型,而是一些奇奇怪怪的东西了?为了找到相应的原因,我们必须进入到SimpleDateFormat的源代码来进行分析。
SimpleDateFormat 多线程问题分析
首先我们进入SimpleDateFormat的parse方法,然后查看parse()接近最后的部分
@Override
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;
}
最后的返回值是通过 calb.establish(calendar).getTime() 获取到的;
接下来我们分析一下CalendarBuilder的这个方法
Calendar establish(Calendar cal) {
boolean weekDate = isSet(WEEK_YEAR)
&& field[WEEK_YEAR] > field[YEAR];
if (weekDate && !cal.isWeekDateSupported()) {
// Use YEAR instead
if (!isSet(YEAR)) {
set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
}
weekDate = false;
}
cal.clear();
// Set the fields from the min stamp to the max stamp so that
// the field resolution works in the Calendar.
for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
for (int index = 0; index <= maxFieldIndex; index++) {
if (field[index] == stamp) {
cal.set(index, field[MAX_FIELD + index]);
break;
}
}
}
if (weekDate) {
int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
int dayOfWeek = isSet(DAY_OF_WEEK) ?
field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
if (dayOfWeek >= 8) {
dayOfWeek--;
weekOfYear += dayOfWeek / 7;
dayOfWeek = (dayOfWeek % 7) + 1;
} else {
while (dayOfWeek <= 0) {
dayOfWeek += 7;
weekOfYear--;
}
}
dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
}
cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
}
return cal;
}
可以看到,在establish中先调用了cal.clear(),后调用了cal.set(),cal 是Calendar的对象,是一个
成员实例。
不幸的是Calendar是线程不安全的,当多个线程同时操作一个SimpleDateFormat的时候,会引起cal的混乱。
SimpleDateFormat 多线程问题解决
从上面我们验证的SimpleDateFormat的线程问题,并且简单的分析了造成线程不安全问题的原因。
那么我们如何在代码编写工作中去规避这个问题了?
当然解决问题的方式永远是多种多样的,下面只是简单的列举了众多解决方法中的几种,如果你有更加好的解决办法,请及时留言,我将感谢万分!!!
局部变量法
package com.java.thread.demo003;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
*
* SimpleDateFormat 线程不安全问题重现
* */
public class Demo003_b {
public static void main(String[] args) {
ExecutorService executors = Executors.newCachedThreadPool();
for(int i=0; i< 10; i++){
executors.execute(()->{
SimpleDateFormat simpleDateFormat
= new SimpleDateFormat("yyyy-MM-dd");
try {
simpleDateFormat.parse("2023-02-08");
System.out.println(Thread.currentThread().getName() + " 日期格式化成功");
} catch (ParseException e) {
System.out.println(Thread.currentThread().getName() + " 解析失败");
e.printStackTrace();
System.exit(1);
}catch (NumberFormatException e) {
System.out.println(Thread.currentThread().getName() + " 解析失败");
e.printStackTrace();
System.exit(1);
}
});
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
executors.shutdown();
System.out.println("所有线程格式化日期成功!!");
}
}
很显然这样能够规避 SimpleDateFormat 线程不安全的问题,但是这对于高并发场景并不友好;因为其会创建很多的SimpleDateFormat对象。
synchronized 锁方式
package com.java.thread.demo003;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
*
* SimpleDateFormat 线程不安全问题重现
* */
public class Demo003_c {
private static SimpleDateFormat simpleDateFormat
= new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) {
ExecutorService executors = Executors.newCachedThreadPool();
for(int i=0; i< 10; i++){
executors.execute(()->{
try {
synchronized (simpleDateFormat) {
simpleDateFormat.parse("2023-02-08");
}
System.out.println(Thread.currentThread().getName() + " 日期格式化成功");
} catch (ParseException e) {
System.out.println(Thread.currentThread().getName() + " 解析失败");
e.printStackTrace();
System.exit(1);
}catch (NumberFormatException e) {
System.out.println(Thread.currentThread().getName() + " 解析失败");
e.printStackTrace();
System.exit(1);
}
});
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
executors.shutdown();
System.out.println("所有线程格式化日期成功!!");
}
}
这种方式虽然不用像局部变量法那样创建很多的对象,但是会造成其它线程的等待,同样对高并发场景不太友好。
ThreadLocal方式
package com.java.thread.demo003;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
*
* SimpleDateFormat 线程不安全问题重现
* */
public class Demo003_d {
private static final ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public static void main(String[] args) {
ExecutorService executors = Executors.newCachedThreadPool();
for(int i=0; i< 10; i++){
executors.execute(()->{
try {
threadLocal.get().parse("2023-02-08");
System.out.println(Thread.currentThread().getName() + " 日期格式化成功");
} catch (ParseException e) {
System.out.println(Thread.currentThread().getName() + " 解析失败");
e.printStackTrace();
System.exit(1);
}catch (NumberFormatException e) {
System.out.println(Thread.currentThread().getName() + " 解析失败");
e.printStackTrace();
System.exit(1);
}
});
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
executors.shutdown();
System.out.println("所有线程格式化日期成功!!");
}
}
使用ThreadLocal存储每个线程拥有的SimpleDateFormat的对象的副本,能够有效的避免线程不安全的问题。
这种方式运行效率比较高,推荐在高并发业务场景使用。
DateTimeFormatter方式
DateTimeFormatter 是java8新提供的日期格式化武器;是线程安全的,且适合高并发场景。
package com.java.thread.demo003;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
*
* SimpleDateFormat 线程不安全问题重现
* */
public class Demo003_e {
private static DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static void main(String[] args) {
ExecutorService executors = Executors.newCachedThreadPool();
for(int i=0; i< 10; i++){
executors.execute(()->{
try {
LocalDate.parse("2023-02-08",formatter);
System.out.println(Thread.currentThread().getName() + " 日期格式化成功");
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + " 解析失败");
e.printStackTrace();
System.exit(1);
}
});
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
executors.shutdown();
System.out.println("所有线程格式化日期成功!!");
}
}
参考
<<深入理解高并发编程>>