并發編程之 ThreadLocal

前言
了解過 SimpleDateFormat 時間工具類的朋友都知道,該工具類非常好用,可以利用該類可以將日期轉換成文本,或者將文本轉換成日期,時間戳同樣也可以 。
以下代碼,我們采用通用的 SimpleDateFormat 對象,在線程池 threadPool中,將對應的 i 值調用 sec2Date 方法來實現日期轉換 , 并且 sec2Date 方法是用 synchronized 修飾的,在多線程競爭的場景下,來達到線程安全的目的 。
【并發編程之 ThreadLocal】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;}}但是加 synchronized 這種方式雖然也能保證線程安全,但是這種方式效率會比較低,畢竟同一時刻下,只能有一個線程能夠執行程序,這顯然不是最好的方案 , 下面我們來了解下更高效的方式,就是利用 ThreadLocal 類來實現 。
ThreadLocal介紹:每個線程需要一個獨享的對象,每個 Thread內有自己的實例副本,這些實例副本是不共享的,讓某個需要用到的對象在線程間隔離,即每個線程都有自己的獨立的對象 。

推薦閱讀