SimpleDateFormat在多线程下存在并发安全问题。
1.重现SimpleDateFormat类的线程安全问题
/**
*
* 重现SimpleDateFormat类的线程安全问题
*
* @author 二十
* @since 2021/9/12 10:05 下午
*/
public class SdfTest {
/**执行总次数*/
private static final int EXECUTE_COUNT=1000;
/**并发线程数*/
private static final int THREAD_COUNT=100;
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
@Test
public void test()throws Exception{
final Semaphore semaphore =new Semaphore(THREAD_COUNT);
final CountDownLatch cdl = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++) {
executor.execute(()->{
try {
semaphore.acquire();
sdf.parse("2021-09-15");
}catch (Exception e){
System.out.println(Thread.currentThread().getName()+"格式化时间失败!");
}finally {
semaphore.release();
cdl.countDown();
}
});
}
cdl.await();
executor.shutdown();
}
}
说明:在高并发下使用SimpleDateFormat类格式化日期的时候抛出了异常,SimpleDateFormat类不是线程安全的! 为什么SimpleDateFormat不是线程安全的?
2.SimpleDateFormat为什么不是线程安全的?
public Date parse(String source) throws ParseException
{
ParsePosition pos = new ParsePosition(0);
//调用了parse方法,最终的实现类在SimpleDateFormat类中
Date result = parse(source, pos);
if (pos.index == 0)
throw new ParseException("Unparseable date: \"" + source + "\"" ,
pos.errorIndex);
return result;
}
点进去parse(),这是一个抽象的方法。
public abstract Date parse(String source, ParsePosition pos);
最终的实现类还是在SimpleDateFormat类中。
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;
}
}
}
//破坏了线程的安全性
pos.index = start;
Date parsedDate;
try {
parsedDate = calb.establish(calendar).getTime();
if (ambiguousYear[0]) {
if (parsedDate.before(defaultCenturyStart)) {
parsedDate = calb.addYear(100).establish(calendar).getTime();
}
}
}
catch (IllegalArgumentException e) {
//破坏了线程的安全性
pos.errorIndex = start;
//破坏了线程的安全性
pos.index = oldStart;
return null;
}
return parsedDate;
}
通过对SimpleDateFormat类中的parse()进行分析可以得知:parse()中存在几处为ParsePosition类中的索引赋值的操作。
一旦将SimpleDateFormat类定义成全局静态变量,那么SimpleDateFormat类在多线程之间是共享的,这就会导致ParsePosition类在多线程之间共享。
在高并发场景下,一个线程对ParsePosition类中的索引进行修改,一定会影响到其他线程对ParsePosition类中索引的读。这就造成了线程的安全问题。
那么在确定了SimpleDateFormat线程不安全的原因以后,如何来解决这个问题。
3.SimpleDateFormat类线程安全的解决
3.1 局部变量法
public class SdfTest {
/**执行总次数*/
private static final int EXECUTE_COUNT=1000;
/**并发线程数*/
private static final int THREAD_COUNT=100;
@Test
public void test()throws Exception{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
final Semaphore semaphore =new Semaphore(THREAD_COUNT);
final CountDownLatch cdl = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++) {
executor.execute(()->{
try {
semaphore.acquire();
sdf.parse("2021-09-15");
}catch (Exception e){
System.out.println(Thread.currentThread().getName()+"格式化时间失败!");
}finally {
semaphore.release();
cdl.countDown();
}
});
}
cdl.await();
executor.shutdown();
}
}
这种方式在高并发场景下会创建大量的SimpleDateFormat对象,影响程序的性能,所以,这种方式在实际生产环境不太推荐。
3.2 synchronized
public class SdfTest {
/**执行总次数*/
private static final int EXECUTE_COUNT=1000;
/**并发线程数*/
private static final int THREAD_COUNT=100;
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");;
@Test
public void test()throws Exception{
final Semaphore semaphore =new Semaphore(THREAD_COUNT);
final CountDownLatch cdl = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++) {
executor.execute(()->{
try {
semaphore.acquire();
synchronized (sdf){
sdf.parse("2021-09-15");
}
}catch (Exception e){
System.out.println(Thread.currentThread().getName()+"格式化时间失败!");
}finally {
semaphore.release();
cdl.countDown();
}
});
}
cdl.await();
executor.shutdown();
}
}
这种方式解决了他的线程安全问题,但是程序在执行的过程中,为SimpleDateFormat类对象加了synchronized锁,导致在同一时刻只能由一个线程执行格式化时间的方法。此时会影响程序的性能,在要求高并发的生产环境下,此种方式也是不太推荐使用的。
3.3 Lock锁方式
public class SdfTest {
/**
* 执行总次数
*/
private static final int EXECUTE_COUNT = 1000;
/**
* 并发线程数
*/
private static final int THREAD_COUNT = 100;
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
private static Lock lock = new ReentrantLock();
@Test
public void test() throws Exception {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch cdl = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++) {
executor.execute(() -> {
try {
semaphore.acquire();
lock.lock();
try {
sdf.parse("2021-09-15");
} finally {
lock.unlock();
}
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + "格式化时间失败!");
} finally {
semaphore.release();
cdl.countDown();
}
});
}
cdl.await();
executor.shutdown();
}
}
通过代码得知:首先定义了Lock类型的全局静态变量作为加锁和释放锁的句柄。然后再SimpleDateFormat.parse()代码执行之前加锁。
这里需要注意的一点是:为了防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到finally代码块。
此种方式在并发下同样影响性能,不太推荐在高并发生产中使用。
3.4ThreadLocal方式
public class SdfTest {
/**
* 执行总次数
*/
private static final int EXECUTE_COUNT = 1000;
/**
* 并发线程数
*/
private static final int THREAD_COUNT = 100;
private static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
@Test
public void test() throws Exception {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch cdl = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_COUNT; i++) {
executor.execute(() -> {
try {
semaphore.acquire();
threadLocal.get().parse("2021-09-15");
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + "格式化时间失败!");
} finally {
threadLocal.remove();
semaphore.release();
cdl.countDown();
}
});
}
cdl.await();
executor.shutdown();
}
}
使用ThreadLocal将每个线程使用的SimpleDateFormat副本保存在ThreadLocal中,各个线程在使用时互不干扰,从而解决了线程安全的问题。
此种方式运行效率比较高,推荐在高并发场景下使用。
3.5 DateTimeFormatter
jdk8线程安全的时间日期API。