揭秘ThreadLocal
ThreadLocal是開發中最常用的技術之一,也是面試重要的考點。本文將由淺入深,介紹ThreadLocal的使用方式、實現原理、內存泄漏問題以及使用場景。
ThreadLocal作用
在并發編程中時常有這樣一種需求:每條線程都需要存取一個同名變量,但每條線程中該變量的值均不相同。
如果是你,該如何實現上述功能?常規的思路如下: 使用一個線程共享的Map<Thread,Object>,Map中的key為線程對象,value即為需要存儲的值。那么,我們只需要通過map.get(Thread.currentThread())即可獲取本線程中該變量的值。
這種方式確實可以實現我們的需求,但它有何缺點呢?——答案就是:需要同步,效率低!
由于這個map對象需要被所有線程共享,因此需要加鎖來保證線程安全性。當然我們可以使用java.util.concurrent.*包下的ConcurrentHashMap提高并發效率,但這種方法只能降低鎖的粒度,不能從根本上避免同步鎖。而JDK提供的ThreadLocal就能很好地解決這一問題。下面來看看ThreadLocal是如何高效地實現這一需求的。
如何使用ThreadLocal
在介紹ThreadLocal原理之前,首先簡單介紹一下它的使用方法。
public class Main{private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();public void start() {for (int i=0; i<10; i++) {new Thread(new Runnable(){public void run(){threadLocal.set(i);threadLocal.get();threadLocal.remove();}}).start();}} } 復制代碼- 首先我們需要創建一個線程共享的ThreadLocal對象,該對象用于存儲Integer類型的值;
- 然后在每條線程中可以通過如下方法操作ThreadLocal:
- set(obj):向當前線程中存儲數據
- get():獲取當前線程中的數據
- remove():刪除當前線程中的數據
ThreadLocal的使用方法非常簡單,關鍵在于它背后的實現原理。回到上面的問題:ThreadLocal究竟是如何避免同步鎖,從而保證讀寫的高效?
ThreadLocal實現原理
ThreadLocal的內部結構如下圖所示:
ThreadLocal并不維護ThreadLocalMap,并不是一個存儲數據的容器,它只是相當于一個工具包,提供了操作該容器的方法,如get、set、remove等。而ThreadLocal內部類ThreadLocalMap才是存儲數據的容器,并且該容器由Thread維護。
每一個Thread對象均含有一個ThreadLocalMap類型的成員變量threadLocals,它存儲本線程中所有ThreadLocal對象及其對應的值。
ThreadLocalMap由一個個Entry對象構成,Entry的代碼如下:
static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;} } 復制代碼Entry繼承自WeakReference<ThreadLocal<?>>,一個Entry由ThreadLocal對象和Object構成。由此可見,Entry的key是ThreadLocal對象,并且是一個弱引用。當沒指向key的強引用后,該key就會被垃圾收集器回收。
那么,ThreadLocal是如何工作的呢?下面來看set和get方法。
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value); }public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {("unchecked")T result = (T)e.value;return result;}}return setInitialValue(); }ThreadLocalMap getMap(Thread t) {return t.threadLocals; } 復制代碼當執行set方法時,ThreadLocal首先會獲取當前線程對象,然后獲取當前線程的ThreadLocalMap對象。再以當前ThreadLocal對象為key,將值存儲進ThreadLocalMap對象中。
get方法執行過程類似。ThreadLocal首先會獲取當前線程對象,然后獲取當前線程的ThreadLocalMap對象。再以當前ThreadLocal對象為key,獲取對應的value。
由于每一條線程均含有各自私有的ThreadLocalMap容器,這些容器相互獨立互不影響,因此不會存在線程安全性問題,從而也無需使用同步機制來保證多條線程訪問容器的互斥性。
為何要使用弱引用?
對弱引用不了解的同學可以參考筆者的另一篇文章:http://blog.csdn.net/u010425776/article/details/50760053。
Java設計之初的一大宗旨就是——弱化指針。 Java設計者希望通過合理的設計簡化編程,讓程序員無需處理復雜的指針操作。然而指針是客觀存在的,在目前的Java開發中也不可避免涉及到“指針操作”。如:
Object a = new Object(); 復制代碼上述代碼創建了一個強引用a,只要強引用存在,垃圾收集器是不會回收該對象的。如果該對象非常龐大,那么為了節約內存空間,在該對象使用完成后,我們需要手動拆除該強引用,如下面代碼所示:
a = null; 復制代碼此時,指向該對象的強引用消除了,垃圾收集器便可以回收該對象。但在這個過程中,仍然需要程序員處理指針。為了弱化指針這一概念,弱引用便出現了,如下代碼創建了一個Person類型的弱引用:
WeakReference<Person> wr = new WeakReference<Person>(new Person()); 復制代碼此時程序員不用再關注指針,只要沒有強引用指向Person對象,垃圾收集器每次運行都會自動將該對象釋放。
那么,ThreadLocalMap中的key使用弱引用的原因也是如此。當一條線程中的ThreadLocal對象使用完畢,沒有強引用指向它的時候,垃圾收集器就會自動回收這個Key,從而達到節約內存的目的。
那么,問題又來了——這會導致內存泄漏問題!
ThreadLocal的內存泄漏問題
在ThreadLocalMap中,只有key是弱引用,value仍然是一個強引用。當某一條線程中的ThreadLocal使用完畢,沒有強引用指向它的時候,這個key指向的對象就會被垃圾收集器回收,從而這個key就變成了null;然而,此時value和value指向的對象之間仍然是強引用關系,只要這種關系不解除,value指向的對象永遠不會被垃圾收集器回收,從而導致內存泄漏!
不過不用擔心,ThreadLocal提供了這個問題的解決方案。
每次操作set、get、remove操作時,ThreadLocal都會將key為null的Entry刪除,從而避免內存泄漏。
那么問題又來了,如果一個線程運行周期較長,而且將一個大對象放入LocalThreadMap后便不再調用set、get、remove方法,此時該仍然可能會導致內存泄漏。
這個問題確實存在,沒辦法通過ThreadLocal解決,而是需要程序員在完成ThreadLocal的使用后要養成手動調用remove的習慣,從而避免內存泄漏。
ThreadLocal的使用場景
Web系統Session的存儲就是ThreadLocal一個典型的應用場景。
Web容器采用線程隔離的多線程模型,也就是每一個請求都會對應一條線程,線程之間相互隔離,沒有共享數據。這樣能夠簡化編程模型,程序員可以用單線程的思維開發這種多線程應用。
當請求到來時,可以將當前Session信息存儲在ThreadLocal中,在請求處理過程中可以隨時使用Session信息,每個請求之間的Session信息互不影響。當請求處理完成后通過remove方法將當前Session信息清除即可。
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的揭秘ThreadLocal的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从运维的角度理解Iaas、Paas、Sa
- 下一篇: PHPer 面试指南 扩展阅读资源整理