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");@Testpublic 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;@Testpublic 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");;@Testpublic 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();@Testpublic 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"));@Testpublic 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。
