目錄
Thread、ThreadLocal及ThreadLocalMap的關係
ThreadLocal的使用場景
- ThreadLocal 用作保存每個線程獨享的對象,爲每個線程都創建一個副本,這樣每個線程都可以修改自己所擁有的副本, 而不會影響其他線程的副本,確保了線程安全。
- ThreadLocal 用作每個線程內需要獨立保存信息,以便供其他方法更方便地獲取該信息的場景。每個線程獲取到的信息可能都是不一樣的,前面執行的方法保存了信息後,後續方法可以通過 ThreadLocal 直接獲取到,避免了傳參,類似於全局變量的概念。
通過例子驗證一下:
我們知道SimpleDateFormat在多線程併發訪問下會出現線程安全問題。
/**
* 線程不安全demo
*
* @author hujy
* @version 1.0
* @date 2020-06-29 20:57
*/
public class ThreadNotSafeDemo {
private static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
public String date(int seconds) {
// 創建不同的date
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(() -> {
String date = new ThreadNotSafeDemo().date(finalI);
System.out.println(date);
});
}
threadPool.shutdown();
}
}
打印運行結果:
上面代碼中每次循環都會創建不同的date對象,但是在多線程併發創建的場景下,打印的結果中出現了大量重複值,說明產生了線程安全問題。
通過ThreadLocal保證線程安全:
/**
* ThreadLocal線程安全demo
*
* @author hujy
* @version 1.0
* @date 2020-06-29 21:19
*/
public class ThreadSafeDemo {
private static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(() -> {
try {
String date = new ThreadSafeDemo().date(finalI);
System.out.println(date);
} finally {
ThreadSafeFormatter.dateFormatThreadLocal.remove();
}
});
}
threadPool.shutdown();
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
}
打印運行結果:
運行結果都是唯一的值,說明通過ThreadLocal可以實現共享變量的線程安全。
ThreadLocal與synchronized的區別
- ThreadLocal 是通過讓每個線程獨享自己的副本,避免了資源的競爭。
- synchronized 主要用於臨界資源的分配,在同一時刻限制最多隻有一個線程能訪問該資源。
- ThreadLocal 並不是用來解決共享資源的多線程訪問的問題,因爲每個線程中的資源只是副本,並不共享。因此ThreadLocal適合作爲線程上下文變量,簡化線程內傳參。
Thread、ThreadLocal及ThreadLocalMap的關係
想要了解Threadlocal的工作原理,就必須瞭解Thread、ThreadLocal以及ThreadLocalMap這三個類之間的關係。
ThreadLocalMap是ThreadLocal類的靜態內部類,本質是一個Map,key的類型就是我們定義的ThreadLocal對象,value則是我們具體要保存的變量參數。
public class ThreadLocal<T> {
...
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
...
private Entry[] table;
}
}
而Thread中含有ThreadLocal.ThreadLocalMap類型的成員變量threadLocals。
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
因此這三個類的關係可以總結爲:
一個 Thread 裏面只有一個ThreadLocalMap ,而在一個 ThreadLocalMap 裏面卻可以有很多的 ThreadLocal,每一個 ThreadLocal 都對應一個 value。因爲一個 Thread 是可以調用多個 ThreadLocal 的,所以 Thread 內部就採用了 ThreadLocalMap 這樣 Map 的數據結構來存放 ThreadLocal 和 value。
另外ThreadLocalMap在解決hash衝突的方式與HashMap不同,HashMap採用的是拉鍊發,而ThreadLocalMap採用的是線性探索法,即發生衝突時,向下繼續尋找空的位置。
調用remove()方法避免內存泄漏
通過ThreadLocalMap的源碼可以看到,Entry中的key被定義爲弱引用類型,當發生GC時,key會被直接回收,無需手動清理。
而value屬於強引用類型,被當前的Thread對象關聯,所以說value的回收取決於Thread對象的生命週期。如果說一個線程執行完畢,線程Thread隨之被釋放,那麼value便不存在內存泄漏的問題。然而,我們一般會通過線程池的方式來複用Thread對象來節省資源,這就會導致一個Thread對象的生命週期會非常長,隨着任務的執行,value就有可能越來越多且無法釋放,最終導致內存泄漏。
因此,我們在使用完ThreadLocal變量後,要手動調用remove()方法來清理ThreadLocalMap(一般在finally代碼塊中)。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}