線程本地變量,每一個線程保存變量的副本,對副本的改動,對其餘的線程而言是透明的(即隔離的)java
使用方式也比較簡單,經常使用的三個方法web
// 設置當前線程的線程局部變量的值 void set(Object value); // 該方法返回當前線程所對應的線程局部變量 public Object get(); // 將當前線程局部變量的值刪除 public void remove();
下面給個實例,來瞅一下,這個東西通常的使用姿式。一般要獲取線程變量, 直接調用 ParamsHolder.get()
算法
public class ParamsHolder { private static final ThreadLocal<Params> PARAMS_INFO = new ThreadLocal<>(); @ToString @Getter @Setter public static class Params { private String mk; } public static void setParams(Params params) { PARAMS_INFO.set(params); } public static void clear() { PARAMS_INFO.remove(); } public static Params get() { return PARAMS_INFO.get(); } public static void main(String[] args) { Thread child = new Thread(new Runnable() { @Override public void run() { System.out.println("child thread initial: " + ParamsHolder.get()); ParamsHolder.setParams(new ParamsHolder.Params("thread")); System.out.println("child thread final: " + ParamsHolder.get()); } }); child.start(); System.out.println("main thread initial: " + ParamsHolder.get()); ParamsHolder.setParams(new ParamsHolder.Params("main")); System.out.println("main thread final: " + ParamsHolder.get()); } }
輸出結果數組
child thread initial: null main thread initial: null child thread final: ParamsHolder.Params(mk=thread) main thread final: ParamsHolder.Params(mk=main)
直接看源碼中的兩個方法, get/set, 看下究竟是如何實現線程變量的安全
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(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) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
邏輯比較清晰多線程
threadLocals
屬性ThreadLocalMap
threadLocals屬性ide
這個屬性的解釋以下,簡單來說,這個裏面的變量都是線程獨享的,徹底由線程本身hold住學習
ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.this
接下來須要瞭解的就是ThreadLocalMap
這個對象的內部構造了,裏面的有個table對象,維護了一個Entry的數組table
,Entry
的key爲ThreadLocal
對象,value爲具體的值。線程
//ThreadLocalMap.java static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; private void set(ThreadLocal<?> key, Object value) { 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)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
聚焦在 int i = key.threadLocalHashCode & (table.length - 1);
這一行,這個就是獲取Entry對象在table
中索引值的主要邏輯,主要利用當前線程的hashCode值
假設出現兩個不一樣的線程,這個code值同樣,會如何?
這種相似hash碰撞的場景,會調用 nextIndex
來獲取下一個位置
針對上面的邏輯,有點有必要繼續研究下, hashCode
的計算方式, 爲何要和數組的長度進行與計算
做爲ThreadLocal實例的變量只有 threadLocalHashCode 這一個,
nextHashCode
和HASH_INCREMENT
是ThreadLocal類的靜態變量,實際上HASH_INCREMENT
是一個常量,表示了連續分配的兩個ThreadLocal實例的threadLocalHashCode值的增量,而nextHashCode 的表示了即將分配的下一個ThreadLocal實例的threadLocalHashCode 的值
全部ThreadLocal對象共享一個AtomicInteger對象nextHashCode用於計算hashcode,一個新對象產生時它的hashcode就肯定了,算法是從0開始,以HASH_INCREMENT = 0x61c88647爲間隔遞增,這是ThreadLocal惟一須要同步的地方。根據hashcode定位桶的算法是將其與數組長度-1進行與操做
ThreadLocalMap的初始長度爲16,每次擴容都增加爲原來的2倍,即它的長度始終是2的n次方,上述算法中使用0x61c88647可讓hash的結果在2的n次方內儘量均勻分佈,減小衝突的機率
ThreadLocal
的注意事項這裏主要的一個問題是線程複用時, 若是不清除掉ThreadLocal 中的值,就會有可怕的事情發生, 先簡單的演示一下
private static final ThreadLocal<AtomicInteger> threadLocal =new ThreadLocal<AtomicInteger>() { @Override protected AtomicInteger initialValue() { return new AtomicInteger(0); } }; static class Task implements Runnable { @Override public void run() { AtomicInteger s = threadLocal.get(); int initial = s.getAndIncrement(); // 指望初始爲0 System.out.println(initial); } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.shutdown(); }
輸出結果
0 0 1
說好的線程變量,這裏竟然沒有按照咱們預期的來玩,主要緣由就是線程複用了,而線程中的局部變量沒有清零,致使下一個使用這個線程的時候,這些局部變量也帶過來,致使沒有按照咱們的預期使用
這個最可能致使的一個超級嚴重的問題,就是web應用中的用戶串掉的問題,若是咱們將每一個用戶的信息保存在 ThreadLocal
中, 若是出現線程複用了,那麼問題就會致使明明是張三用戶,結果登陸顯示的是李四的賬號,這下就真的呵呵了
所以,強烈推薦,對於線程變量,一但不用了,就顯示的調用 remove()
方法進行清楚
SimpleDataFormate
是一個非線程安全的類,可使用 ThreadLocal 完成的線程安全的使用
public class ThreadLocalDateFormat { static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String date2String(Date date) { return sdf.get().format(date); } public static Date string2Date(String str) throws ParseException { return sdf.get().parse(str); } }
想想,爲何這種方式是線程安全的呢?
ThreadLocal 線程本地變量,每一個線程保存變量的副本,對副本的改動,對其餘的線程而言是透明的(即隔離的)
三個經常使用的方法
// 設置當前線程的線程局部變量的值 void set(Object value); // 該方法返回當前線程所對應的線程局部變量 public Object get(); // 將當前線程局部變量的值刪除 public void remove();
利用了HashMap的設計理念,一個map中存儲Thread->線程變量的映射關係, 所以線程變量在多線程之間是隔離的
一般建議是線程執行完畢以後,主動去失效掉ThreadLocal中的變量,以防止線程複用致使變量被亂用了
盡信書則不如,已上內容,純屬一家之言,因本人能力通常,見識有限,若有問題,請不吝指正,感激