(十三)Thread-Specific Storage(ThreadLocal)模式
一、定義
Thread-Specific Storage就是“線程獨有的存儲庫”,該模式會對每個線程提供獨有的內存空間。java.lang.ThreadLocal類提供了該模式的實現,ThreadLocal的實例是一種集合(collection)架構,該實例管理了很多對象,可以想象成一個保管有大量保險箱的房間。
?
java.lang.ThreadLocal類的方法:
- public void set()
 
該方法會檢查當前調用線程,默認以該線程的Thread.currentThread()值作為鍵,來保存指定的值。
- public Object get()
 
該方法會檢查當前調用線程,默認以該線程的Thread.currentThread()值作為鍵,獲取保存指定的值。
?
?
?
二、模式案例
?
//實際執(zhí)行記錄日志的類,每個線程都會擁有該類的實例 public class TSLog {private PrintWriter writer = null;public TSLog(String filename){try {writer = new PrintWriter(new FileWriter(filename));} catch (IOException e) {e.printStackTrace();}}public void println(String s){writer.println(s);}public void close(){writer.println("==== End of log ====");writer.close();} }?
public class Log {private static final ThreadLocal<TSLog> tsLogCollection = new ThreadLocal<TSLog>();public static void println(String s){getTSLog().println(s);}private static TSLog getTSLog() {TSLog tsLog = tsLogCollection.get();if(tsLog==null){tsLog = new TSLog(Thread.currentThread().getName()+"-log.txt");tsLogCollection.set(tsLog);}return tsLog;}public static void close(){getTSLog().close();} }?
public class ClientThread extends Thread {public ClientThread(String name) {super(name);}@Overridepublic void run() {System.out.println(getName()+" BEGIN");for (int i = 0; i < 10; i++) {Log.println("i = "+i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}Log.close();System.out.println(getName()+" END");} }?
public class Main {public static void main(String[] args) {new ClientThread("Alice").start();new ClientThread("Bobby").start();new ClientThread("Chris").start();} }?
?
打開硬盤里的項目目錄,可以看到三個生成的日志文件
文件內容都是以下圖示
?
?
Alice、Boddy、Chris三個線程調用Log類的同一個方法,但實際上每個線程都擁有獨自的TSLog實例。
?
?
?
?
三、模式講解
Thread-Specific Storage模式的角色如下:
- Client(委托人)
 
Client會將工作委托給TSObjectProxy。(案例中的ClientThread類就是Client)
- TSObjectProxy(線程獨有對象的代理者)
 
TSObjectProxy會處理多個Client委托的工作。(案例中的Log類就是TSObjectProxy)
- TSObjectCollection(線程獨有對象的集合)
 
案例中的java.lang.ThreadLocal類就是TSObjectCollection
- TSObject(線程獨有的對象)
 
TSObject存放線程所持有的信息,TSObject實例的方法只會由單線程調用,由TSObjectCollection管理,每個線程都擁有獨立的TSObject實例。(案例中的TSLog類就是TSObject)
?
?
?
?
四、ThreadLocal的原理
JDK中有一個類就實現了Thread-Specific Storage模式,即ThreadLocal,ThreadLocal類主要有四個方法:
1、初始化返回值的方法:
 該方法實現只返回 null,并且修飾符為protected,很明顯,如果用戶想返回初始值不為null,則需要重寫該方法;
?
2、get方法,獲取線程本地副本變量
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {T result = (T)e.value;return result;}}return setInitialValue(); }?
3、set方法,設置線程本地副本變量
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value); }?
4、remove方法,移除線程本地副本變量
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this); }?
4.2 實現原理
如果需要我們自己來設計ThreadLocal對象,那么,一般的實現思路:設計一個線程安全的Map,key就是當前線程對象,Value就是線程本地變量的值。
然而,JDK的實現思路:
讓每個Thread對象,自身持有一個Map,這個Map的Key就是當前ThreadLocal對象,Value是本地線程變量值。相對于加鎖的實現方式,這樣做可以提升性能,其實是一種以時間換空間的思路。
?
ThreadLocal類有個getMap()方法,其實就是返回Thread對象自身的Map——threadLocals。
ThreadLocalMap getMap(Thread t) {return t.threadLocals; }set方法如下:
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}?get方法如下:
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}看了源碼就能懂了,假如線程有變量A和變量B,那么變量A會使用ThreadLocalA的set方法存放,變量B會使用ThreadLocalB的set方法存放,而set方法實際上是將變量A存放在線程自己的Map里,key為ThreadLocalA,value是變量A。
?
threadLocals是一種ThreadLocal.ThreadLocalMap類型的數據結構,作為內部類定義在ThreadLocal類中,其內部采用一種WeakReference(弱引用)的方式保存鍵值對。
?
?
Entry繼承了WeakReference:
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;} }?
?
4.3 使用注意
Hash沖突
ThreadLocalMap中解決Hash沖突采用線性探測的方式。所謂線性探測:
就是根據初始key的hashcode值確定元素在table數組中的位置,如果發(fā)現這個位置上已經有其他key值的元素被占用,則利用固定的算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。
ThreadLocalMap采用線性探測的方式解決Hash沖突的效率很低(簡單地步長+1),所以如果有大量不同的ThreadLocal對象放入map中時發(fā)送沖突,則效率很低。
?
使用建議
每個線程只存一個變量,這樣的話所有的線程存放到map中的Key都是相同的ThreadLocal,如果一個線程要保存多個變量,就需要創(chuàng)建多個ThreadLocal,多個ThreadLocal放入Map中時會極大的增加Hash沖突的可能。
?
內存泄漏
ThreadLocal在ThreadLocalMap中是以一個弱引用類型被Entry中的Key引用的,因此如果ThreadLocal沒有外部強引用來引用它,那么ThreadLocal會在下次JVM垃圾收集時被回收。
這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。
因此如果當前線程的生命周期很長,一直存在,那么其內部的ThreadLocalMap對象也一直生存下來,這些null key就存在一條強引用鏈的關系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成內存泄漏。
?
?
但JVM團隊已經考慮到這樣的情況,并做了一些措施來保證ThreadLocal盡量不會內存泄漏:
 在ThreadLocal的get()、set()、remove()方法調用的時候會清除掉線程的ThreadLocalMap中所有Entry中Key為null的Value,并將整個Entry設置為null,利于下次內存回收。
?
最好的解決方案:
每次使用完ThreadLocal,都調用它的remove()方法,清除數據。
/*** Remove the entry for key.*/private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}}?
借鑒學習自https://segmentfault.com/a/1190000015558915
?
總結
以上是生活随笔為你收集整理的(十三)Thread-Specific Storage(ThreadLocal)模式的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: eog命令在播放图片时候的用法总结
 - 下一篇: 寻找发帖水王java_SWUST_OJ