Appearance
SimpleDateFormat线程安全问题
1、重现SimpleDateFormat类的线程安全问题
面试都会说SimpateDateFormat线程不安全,那为什么不安全,为了重现SimpleDateFormat类的线程安全问题,一种比较简单的方式就是使用线程池结合Java并发包中的CountDownLatch类和Semaphore类来重现线程安全问题。
java
package com.batch.controller;
import java.text.SimpleDateFormat;
import java.util.concurrent.*;
/**
* @Author: zouming
* @Date: 2024/5/29 0:26
*/
public class SimpleDateFormatDemo {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
//SimpleDateFormat对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 0; i < EXECUTE_COUNT; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
simpleDateFormat.parse("2024-05-29");
} catch (InterruptedException e) {
System.err.println("线程被中断:" + Thread.currentThread().getName());
} catch (Exception e) {
System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
} finally {
semaphore.release();
countDownLatch.countDown();
}
});
}
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("主程序线程被中断");
} finally {
executorService.shutdown();
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("线程池未能在规定时间内关闭");
}
System.out.println("所有线程格式化日期成功");
}
}
}
核心方法
java
simpleDateFormat.parse("2024-05-29");
运行结果
java
Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" 线程:pool-1-thread-7 格式化日期失败
线程:pool-1-thread-9 格式化日期失败
线程:pool-1-thread-10 格式化日期失败
Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-6" 线程:pool-1-thread-15 格式化日期失败
线程:pool-1-thread-21 格式化日期失败
Exception in thread "pool-1-thread-23" 线程:pool-1-thread-16 格式化日期失败
线程:pool-1-thread-11 格式化日期失败
java.lang.ArrayIndexOutOfBoundsException
线程:pool-1-thread-27 格式化日期失败
at java.lang.System.arraycopy(Native Method)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597)
at java.lang.StringBuffer.append(StringBuffer.java:367)
at java.text.DigitList.getLong(DigitList.java:191)线程:pool-1-thread-25 格式化日期失败
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
线程:pool-1-thread-14 格式化日期失败
at java.text.DateFormat.parse(DateFormat.java:364)
at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
线程:pool-1-thread-13 格式化日期失败 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: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
线程:pool-1-thread-20 格式化日期失败 at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
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: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
结论
说明,在高并发下使用SimpleDateFormat类格式化日期时抛出了异常,SimpleDateFormat类不是线程安全的!!!
2、SimpleDateFormat类为何不是线程安全的
SimpleDateFormat是继承自DateFormat类,DateFormat类中维护了一个全局的Calendar变量
java
public abstract class DateFormat extends Format {
/**
* The {@link Calendar} instance used for calculating the date-time fields
* and the instant of time. This field is used for both formatting and
* parsing.
*
* <p>Subclasses should initialize this field to a {@link Calendar}
* appropriate for the {@link Locale} associated with this
* {@code DateFormat}.
* @serial
用于计算日期-时间字段和时间瞬间的 Calendar 实例。此字段用于格式化和解析。
子类应将此字段初始化为适合与此 DateFormat 关联的区域设置的日历。
*/
protected Calendar calendar;
//省略其他代码.....
}
从注释可以看出,这个Calendar对象既用于格式化也用于解析日期时间。SimpleDateFormat 类中 parse()进行格式化
java
/*
分析字符串中的文本以生成 Date。该方法尝试从 pos 给出的索引开始解析文本。如果解析成功,则 pos 的索引将更新为使用的最后一个字符之后的索引(解析不一定使用字符串末尾的所有字符),并返回解析的日期。更新的 pos 可用于指示下次调用此方法的起点。如果发生错误,则不会更改 pos 的索引,将 pos 的错误索引设置为发生错误的字符的索引,并返回 null。此分析操作使用日历生成 Date。在解析之前,将清除日历的所有日期时间字段,并且日历的日期时间字段的默认值将用于任何缺少的日期时间信息。例如,如果解析操作未给出年份值,则解析日期的年份值为 1970 年,而 GregorianCalendar 则为 1970 年。TimeZone 值可能会被覆盖,具体取决于给定的模式和文本中的时区值。可能需要还原之前通过调用 setTimeZone 设置的任何 TimeZone 值才能执行进一步操作。Params: text – 一个字符串,其中的一部分应该被解析。pos – 具有如上所述的索引和错误索引信息的 ParsePosition 对象。返回:从字符串中解析的日期。如果出错,则返回 null。抛出:NullPointerException – 如果 text 或 pos 为 null。
*/
@Override
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:
// Peek the next pattern to determine if we need to
// obey the number of pattern letters for
// parsing. It's required when parsing contiguous
// digit text (e.g., "20010704") with a pattern which
// has no delimiters between fields, like "yyyyMMdd".
boolean obeyCount = false;
// In Arabic, a minus sign for a negative number is put after
// the number. Even in another locale, a minus sign can be
// put after a number using DateFormat.setNumberFormat().
// If both the minus sign and the field-delimiter are '-',
// subParse() needs to determine whether a '-' after a number
// in the given text is a delimiter or is a minus sign for the
// preceding number. We give subParse() a clue based on the
// information in compiledPattern.
boolean useFollowingMinusSignAsDelimiter = false;
if (i < compiledPattern.length) {
int nextTag = compiledPattern[i] >>> 8;
int nextCount = compiledPattern[i] & 0xff;
obeyCount = shouldObeyCount(nextTag, nextCount);
if (hasFollowingMinusSign &&
(nextTag == TAG_QUOTE_ASCII_CHAR ||
nextTag == TAG_QUOTE_CHARS)) {
if (nextTag != TAG_QUOTE_ASCII_CHAR) {
nextCount = compiledPattern[i+1];
}
if (nextCount == 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 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;
}
可见,最后的返回值是通过调用CalendarBuilder.establish()方法获得的,而这个方法的参数正好就是前面的Calendar对象。
接下来,我们再来看看CalendarBuilder.establish()方法
java
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;
}
`
// 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;
}
在CalendarBuilder.establish()方法中先后调用了cal.clear()与cal.set(),也就是先清除cal对象中设置的值,再重新设置新的值。由于Calendar内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个SimpleDateFormat时就会引起cal的值混乱。类似地, format()方法也存在同样的问题。
因此, SimpleDateFormat类不是线程安全的根本原因是:DateFormat类中的Calendar对象被多线程共享,而Calendar对象本身不支持线程安全。
3、解决SimpleDateFormat类的线程安全问题
1.局部变量法
java
package com.batch.controller;
import java.text.SimpleDateFormat;
import java.util.concurrent.*;
/**
* @Author: zouming
* @Date: 2024/5/29 0:26
*/
public class SimpleDateFormatDemo01 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
//SimpleDateFormat对象
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 0; i < EXECUTE_COUNT; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
simpleDateFormat.parse("2024-05-29");
} catch (InterruptedException e) {
System.err.println("线程被中断:" + Thread.currentThread().getName());
} catch (Exception e) {
System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
} finally {
semaphore.release();
countDownLatch.countDown();
}
});
}
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("主程序线程被中断");
} finally {
executorService.shutdown();
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("线程池未能在规定时间内关闭");
}
System.out.println("所有线程格式化日期成功");
}
}
}
这种方式在高并发下会创建大量的SimpleDateFormat类对象,影响程序的性能,所以,这种方式在实际生产环境不太被推荐。
2.synchronized锁方式
java
package com.batch.controller;
import java.text.SimpleDateFormat;
import java.util.concurrent.*;
/**
* @Author: zouming
* @Date: 2024/5/29 0:26
*/
public class SimpleDateFormatDemo02 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
//SimpleDateFormat对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 0; i < EXECUTE_COUNT; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
synchronized (simpleDateFormat){
simpleDateFormat.parse("2020-01-01");
}
} catch (InterruptedException e) {
System.err.println("线程被中断:" + Thread.currentThread().getName());
} catch (Exception e) {
System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
} finally {
semaphore.release();
countDownLatch.countDown();
}
});
}
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("主程序线程被中断");
} finally {
executorService.shutdown();
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("线程池未能在规定时间内关闭");
}
System.out.println("所有线程格式化日期成功");
}
}
}
核心代码
java
synchronized (simpleDateFormat){
simpleDateFormat.parse("2020-01-01");
}
虽然这种方式能够解决SimpleDateFormat类的线程安全问题,但是由于在程序的执行过程中,为SimpleDateFormat类对象加上了synchronized锁,导致同一时刻只能有一个线程执行parse(String)方法。此时,会影响程序的执行性能,在要求高并发的生产环境下,此种方式也是不太推荐使用的
3.Lock锁方式
java
package com.batch.controller;
import java.text.SimpleDateFormat;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: zouming
* @Date: 2024/5/29 0:26
*/
public class SimpleDateFormatDemo03 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
//SimpleDateFormat对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
//Lock对象
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 0; i < EXECUTE_COUNT; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
lock.lock();
simpleDateFormat.parse("2020-01-01");
} catch (InterruptedException e) {
System.err.println("线程被中断:" + Thread.currentThread().getName());
} catch (Exception e) {
System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
} finally {
semaphore.release();
countDownLatch.countDown();
lock.unlock();
}
});
}
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("主程序线程被中断");
} finally {
executorService.shutdown();
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("线程池未能在规定时间内关闭");
}
System.out.println("所有线程格式化日期成功");
}
}
}
此种方式同样会影响高并发场景下的性能,不太建议在高并发的生产环境使用。
4.ThreadLocal方式
使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题。
java
package com.batch.controller;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: zouming
* @Date: 2024/5/29 0:26
*/
public class SimpleDateFormatDemo04 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
//SimpleDateFormat对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
//ThreadLocal
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 0; i < EXECUTE_COUNT; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
threadLocal.get().parse("2020-01-01");
} catch (InterruptedException e) {
System.err.println("线程被中断:" + Thread.currentThread().getName());
} catch (Exception e) {
System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
} finally {
semaphore.release();
countDownLatch.countDown();
}
});
}
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("主程序线程被中断");
} finally {
executorService.shutdown();
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("线程池未能在规定时间内关闭");
}
threadLocal.remove();
System.out.println("所有线程格式化日期成功");
}
}
}
5.DateTimeFormatter方式
DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作
java
package com.batch.controller;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.*;
/**
* @Author: zouming
* @Date: 2024/5/29 0:26
*/
public class SimpleDateFormatDemo05 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
//DateTimeFormatter
private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 0; i < EXECUTE_COUNT; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
LocalDate.parse("2020-01-01",formatter);
} catch (InterruptedException e) {
System.err.println("线程被中断:" + Thread.currentThread().getName());
} catch (Exception e) {
System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
} finally {
semaphore.release();
countDownLatch.countDown();
}
});
}
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("主程序线程被中断");
} finally {
executorService.shutdown();
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("线程池未能在规定时间内关闭");
}
System.out.println("所有线程格式化日期成功");
}
}
}
DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作
6.joda-time方式
joda-time是第三方处理日期时间格式化的类库,是线程安全的
xml
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.9</version>
</dependency>
java
package com.batch.controller;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.concurrent.*;
/**
* @Author: zouming
* @Date: 2024/5/29 0:26
*/
public class SimpleDateFormatDemo06 {
//执行总次数
private static final int EXECUTE_COUNT = 1000;
//同时运行的线程数量
private static final int THREAD_COUNT = 20;
//DateTimeFormatter
private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 0; i < EXECUTE_COUNT; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
DateTime.parse("2020-01-01",dateTimeFormatter).toDate();
} catch (InterruptedException e) {
System.err.println("线程被中断:" + Thread.currentThread().getName());
} catch (Exception e) {
System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
} finally {
semaphore.release();
countDownLatch.countDown();
}
});
}
countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("主程序线程被中断");
} finally {
executorService.shutdown();
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("线程池未能在规定时间内关闭");
}
System.out.println("所有线程格式化日期成功");
}
}
}
注意是用的是joda中的日期类
import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter;
使用joda-time库来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。
4、总结
解决SimpleDateFormat类的线程安全问题的方案总结
在解决解决SimpleDateFormat类的线程安全问题的几种方案中,局部变量法由于线程每次执行格式化时间时,都会创建SimpleDateFormat类的对象,这会导致创建大量的SimpleDateFormat对象,浪费运行空间和消耗服务器的性能,因为JVM创建和销毁对象是要耗费性能的。所以,不推荐在高并发要求的生产环境使用。
synchronized锁方式和Lock锁方式在处理问题的本质上是一致的,通过加锁的方式,使同一时刻只能有一个线程执行格式化日期和时间的操作。这种方式虽然减少了SimpleDateFormat对象的创建,但是由于同步锁的存在,导致性能下降,所以,不推荐在高并发要求的生产环境使用。
ThreadLocal通过保存各个线程的SimpleDateFormat类对象的副本,使每个线程在运行时,各自使用自身绑定的SimpleDateFormat对象,互不干扰,执行性能比较高,推荐在高并发的生产环境使用。
DateTimeFormatter是Java 8中提供的处理日期和时间的类,DateTimeFormatter类本身就是线程安全的推荐在高并发场景下的生产环境使用。
joda-time是第三方处理日期和时间的类库,线程安全,性能经过高并发的考验,推荐在高并发场景下的生产环境使用。