背景:
最近项目引入了 SonarLink,解决代码规范的问题,在检查历史代码的时候,发现了一个问题。
先看代码:
1 public class DateUtil { 2 3 private static final String DATE_FORMAT_1 = "yyyy-MM-dd HH:mm:ss"; 4 private static final String DATE_FORMAT_2 = "yyyy-MM-dd"; 5 6 private static SimpleDateFormat sdf1 = new SimpleDateFormat(DATE_FORMAT_1); 7 private static SimpleDateFormat sdf2 = new SimpleDateFormat(DATE_FORMAT_2); 8 9 private DateUtil() { 10 11 } 12 13 public static String formatDate1(Date date) throws ParseException { 14 return sdf1.format(date); 15 } 16 17 public static String formatDate2(Date date) throws ParseException { 18 return sdf2.format(date); 19 } 20 21 public static Date parseDate1(String dateStr) throws ParseException { 22 return sdf1.parse(dateStr); 23 } 24 25 public static Date parseDate2(String dateStr) throws ParseException { 26 return sdf2.parse(dateStr); 27 } 28 29 }
问题出在什么地方?就出在一个共享的变量 SimpledDateFormat,本身是一个线程不安全的类(由于内部实现使用了 Calendar),导致在多线程情况下可能出错。
多线程检测:
1 public class DateFormatTest { 2 3 public static class TestSimpleDateFormatThreadSafe extends Thread { 4 5 @Override 6 public void run() { 7 while (true) { 8 try { 9 this.join(1000); 10 } catch (InterruptedException e1) { 11 e1.printStackTrace(); 12 } 13 try { 14 System.out.println(this.getName() + ":" + DateUtil.parseDate1("2013-05-24 06:02:20")); 15 } catch (ParseException e) { 16 e.printStackTrace(); 17 } 18 } 19 } 20 } 21 22 public static void main(String[] args) { 23 for (int i = 0; i < 3; i++) { 24 new TestSimpleDateFormatThreadSafe().start(); 25 } 26 } 27 28 }
执行结果如下图(多次执行,出现的结果可能不同):
解决方案:
这种问题,不仅仅会出现在 SimpleDateFormat 中,只能说 SimpleDateFormat 比较常见,具有代表性。
只要是将一个线程不安全类产生的实例作为共享变量,都有可能出现多线程的问题。
出现这类问题,应该从以下3个角度考虑解决方案:
- 规避将一个线程不安全的对象,作为共享变量的情况。
- 从多线程的角度考虑,解决线程安全问题。
- 不使用线程不安全的对象,找一个拥有相同功能的其他类,作为替代方案。
第一类解决方案:
不要将线程不安全的对象,作为共享变量,在方法内部调用的时候再初始化这个对象。
代码如下:
1 public class DateUtil1 { 2 3 private static final String DATE_FORMAT_1 = "yyyy-MM-dd HH:mm:ss"; 4 private static final String DATE_FORMAT_2 = "yyyy-MM-dd"; 5 6 private DateUtil1() { 7 8 } 9 10 public static String formatDate1(Date date) throws ParseException { 11 SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_1); 12 return sdf.format(date); 13 } 14 15 public static String formatDate2(Date date) throws ParseException { 16 SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_2); 17 return sdf.format(date); 18 } 19 20 public static Date parseDate1(String dateStr) throws ParseException { 21 SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_1); 22 return sdf.parse(dateStr); 23 } 24 25 public static Date parseDate2(String dateStr) throws ParseException { 26 SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_2); 27 return sdf.parse(dateStr); 28 } 29 30 }
很显然,大多数情况下,都不会采用这个方案,频繁地创建-销毁对象,对于内存的影响非常大。
第二类解决方案:
从多线程的角度,就是说在使用这个线程不安全类的时候,加以控制,从以下两个角度入手:
- 时间换空间:同步锁 synchronized。
- 空间换时间:独立线程 ThreadLocal。
synchronized
使用 synchronized 的思路很简单,在使用线程不安全变量之前,先将这个变量用 synchronized 关键字锁住。
每一次其他线程使用这个变量时,会等待上一个线程释放这个锁之后再执行。
很明显,在高并发的情况下,这种方案对于时间的消耗很大。
代码如下:
1 public final class DateUtil2 { 2 3 private static final String DATE_FORMAT_1 = "yyyy-MM-dd HH:mm:ss"; 4 private static final String DATE_FORMAT_2 = "yyyy-MM-dd"; 5 6 private static final SimpleDateFormat SDF_1 = new SimpleDateFormat(DATE_FORMAT_1); 7 private static final SimpleDateFormat SDF_2 = new SimpleDateFormat(DATE_FORMAT_2); 8 9 private DateUtil2() { 10 11 } 12 13 public static String formatDate1(Date date) throws ParseException { 14 synchronized (SDF_1) { 15 return SDF_1.format(date); 16 } 17 } 18 19 public static String formatDate2(Date date) throws ParseException { 20 synchronized (SDF_2) { 21 return SDF_2.format(date); 22 } 23 } 24 25 public static Date parseDate1(String dateStr) throws ParseException { 26 synchronized (SDF_1) { 27 return SDF_1.parse(dateStr); 28 } 29 } 30 31 public static Date parseDate2(String dateStr) throws ParseException { 32 synchronized (SDF_2) { 33 return SDF_2.parse(dateStr); 34 } 35 } 36 37 }
ThreadLocal
ThreadLocal,可以简单地这么理解,ThreadLocal 为每一个使用这个线程的变量创建一个独立的副本。
每一个线程都在自己内部改变这个变量,自然不会出现线程安全问题。
明显,这种方案对于内存空间,消耗极大。
代码如下:
1 public final class DateUtil3 { 2 3 private static final String DATE_FORMAT_1 = "yyyy-MM-dd HH:mm:ss"; 4 private static final String DATE_FORMAT_2 = "yyyy-MM-dd"; 5 6 private static Map<String, ThreadLocal<SimpleDateFormat>> threadLocalMap; 7 private static List<String> dateFormatStringList; 8 9 private DateUtil3() { 10 11 } 12 13 static { 14 threadLocalMap = new HashMap<>(); 15 dateFormatStringList = new ArrayList<>(); 16 dateFormatStringList.add(DATE_FORMAT_1); 17 dateFormatStringList.add(DATE_FORMAT_2); 18 for (final String s : dateFormatStringList) { 19 SimpleDateFormat sdf = new SimpleDateFormat(s); 20 ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>() { 21 @Override 22 protected SimpleDateFormat initialValue() { 23 return new SimpleDateFormat(s); 24 } 25 }; 26 threadLocal.set(sdf); 27 threadLocalMap.put(s, threadLocal); 28 } 29 } 30 31 public static String formatDate1(Date date) throws ParseException { 32 SimpleDateFormat sdf = threadLocalMap.get(DATE_FORMAT_1).get(); 33 return sdf.format(date); 34 } 35 36 public static String formatDate2(Date date) throws ParseException { 37 SimpleDateFormat sdf = threadLocalMap.get(DATE_FORMAT_2).get(); 38 return sdf.format(date); 39 } 40 41 public static Date parseDate1(String dateStr) throws ParseException { 42 SimpleDateFormat sdf = threadLocalMap.get(DATE_FORMAT_1).get(); 43 return sdf.parse(dateStr); 44 } 45 46 public static Date parseDate2(String dateStr) throws ParseException { 47 SimpleDateFormat sdf = threadLocalMap.get(DATE_FORMAT_2).get(); 48 return sdf.parse(dateStr); 49 } 50 51 }
第三类解决方案: