并发编程之 ThreadLocal

前言
了解过 SimpleDateFormat 时间工具类的朋友都知道,该工具类非常好用,可以利用该类可以将日期转换成文本,或者将文本转换成日期,时间戳同样也可以 。
以下代码,我们采用通用的 SimpleDateFormat 对象,在线程池 threadPool中,将对应的 i 值调用 sec2Date 方法来实现日期转换,并且 sec2Date 方法是用 synchronized 修饰的,在多线程竞争的场景下,来达到线程安全的目的 。
public class SynchronizedTest {public static ExecutorService threadPool = Executors.newFixedThreadPool(10);public static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");public static void main(String[] args) {for (int i = 0; i < 1000; i++) {int finalI = i;threadPool.submit(() -> System.out.println(finalI + "---" + new ThreadLocal2().sec2Date(finalI)));}threadPool.shutdown();}private synchronized String sec2Date(int seconds) {Date date = new Date(seconds * 1000L);String format = dateFormat.format(date);return format;}}输出结果:

并发编程之 ThreadLocal

文章插图
但是在结果中,我们不难看出,还是会输出重复值,即使我们用了 synchronized 修饰方法,还是会出现线程不安全的情况 。之所以出现这种现象,并非是我们编写的代码出了问题,毕竟在我们平时开发中,通过 synchronized 关键字确实能达到线程安全的目的,这里其实是 SimpleDateFormat 内部并不是线程安全的 导致的 。
主要原因:当两个及以上线程同时使用相同的 SimpleDateFormat 对象(如 static 修饰)的话,就拿上面调用的 format 方法时,format 方法内部就会出现多个线程会同时调用 calendar.setTime 方法时,在多线程竞争的情况下,发生幻读,就会导致重复值的发生 。
下面,我们去看下 SimpleDateFormat 的 format 源码,去探究下为什么会线程不安全 。
并发编程之 ThreadLocal

文章插图
以上源码就是 SimpleDateFormat 类下的 format 方法的源码,我们不需要过多了解里面具体的实现细节,我们只需要关注红色框住的内容,即 calendar.setTime(date);,该 calendar 是 SimpleDateFormat 的父类 DateFormat 定义的一个成员变量 。
并发编程之 ThreadLocal

文章插图
由此我们可以得到一个结论:在多线程竞争的情况下,它们就会共享这个 calendar 成员变量,并去调用它的 calendar.setTime(date) 修改值,这样就会导致 date 变量被其他线程给修改或覆盖掉,就会导致最终的结果会出现重复的情况,因此 SimpleDateFormat 是线程不安全的 。
解决方案一:我们只需要用 synchronized 直接修饰 dateFormat 变量,让每次只有一个线程能够操作 dateFormat 的权利,说白了就是让 synchronized 修饰的这块代码去串行执行,就可以避免发生线程不安全的情况 。
public class SynchronizedTest {public static ExecutorService threadPool = Executors.newFixedThreadPool(10);public static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");public static void main(String[] args) {for (int i = 0; i < 1000; i++) {int finalI = i;threadPool.submit(() -> System.out.println(finalI + "---" + new SynchronizedTest().sec2Date(finalI)));}threadPool.shutdown();}private String sec2Date(int seconds) {Date date = new Date(seconds * 1000L);String format;synchronized (dateFormat) {format = dateFormat.format(date);}return format;}}解决方案二:原理如同方案一相同(一个是锁住 dateFormat 变量,另一个是锁着整个 SynchronizedTest 类)
public class SynchronizedTest {public static ExecutorService threadPool = Executors.newFixedThreadPool(10);public static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");public static void main(String[] args) {for (int i = 0; i < 1000; i++) {int finalI = i;threadPool.submit(() -> System.out.println(finalI + "---" + new SynchronizedTest().sec2Date(finalI)));}threadPool.shutdown();}private String sec2Date(int seconds) {Date date = new Date(seconds * 1000L);String format;synchronized (SynchronizedTest.class) {format = dateFormat.format(date);}return format;}}

经验总结扩展阅读