ThreadLocal以及增强
多線程的本質就是增加任務的并發,提高效率。但是又要控制任務不錯亂,可以通過鎖來控制資源的訪問。
除了控制資源的訪問外,我們可以通過增加資源來保證所有對象的線程安全。比如100個人填寫個人信息表,如果只有一支筆,那么大家都得排隊,如果準備100支筆,這樣人手一支筆,就可以很快完成填寫信息。
如果說鎖是第一種思路,ThreadLocal就是第二種思路。
ThreadLocal
ThreadLocal的簡單示例
從ThreadLocal的名字上可以看到,這是一個線程的局部變量,也就是說只有當前線程可以訪問,自然是線程安全的。
下面來看一個簡單示例:
package main.java.study;import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public class ThreadLocalTest {private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static class ParseDate implements Runnable {int i = 0;public ParseDate(int i) {this.i = i;}public void run() {try {Date t = sdf.parse("2019-05-24 17:00:" + i % 60);System.out.println(i + ":" + t);} catch (ParseException e) {e.printStackTrace();}}}public static void main(String[] args) {// TODO Auto-generated method stubExecutorService es = Executors.newFixedThreadPool(10);for (int i = 0; i < 100; i++) {es.execute(new ParseDate(i));}}}運行結果:
結果中即有正確的,又有錯誤的異常。出現這種問題的原因是SimpleDateFormat.parse()方法并不是線程安全的。因此在線程池中共享這個對象必然導致錯誤。
一種可行的方法是在sdf.parse()方法上加鎖,這是一般思路,這里我們不這么做,我們使用ThreadLocal為每個線程都產生一個SimpleDateFormat對象。
package main.java.study;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalTest2 {
?? ? static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
?? ???? public static class ParseDate implements Runnable {
?? ???????? int i = 0;
?? ???????? public ParseDate(int i) {
?? ???????????? this.i = i;
?? ???????? }
?? ???????? public void run() {
?? ???????????? try {
?? ???????????????? if (tl.get() == null) {
?? ???????????????????? tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); //必須為ThreadLocal分配不同的對象,不然不能保證線程安全。
?? ???????????????? }
?? ???????????????? Date t = tl.get().parse("2019-05-24 17:00:" + i % 60);
?? ???????????????? System.out.println(i + ":" + t);
?? ???????????? } catch (ParseException e) {
?? ???????????????? e.printStackTrace();
?? ???????????? }
?? ???????? }
?? ???? }
?? ??? ?
?? ?public static void main(String[] args) {
?? ??? ?// TODO Auto-generated method stub
?? ??? ?ExecutorService es = Executors.newFixedThreadPool(10);
??????? for (int i = 0; i < 100; i++) {
??????????? es.execute(new ParseDate(i));
??????? }
?? ?}
}
?
執行結果:
從上面可以看出,為每個線程人手分配一個對象工作并不是由ThreadLoca來完成,而是在應用層保證。如果在應用上為每個線程分配了同一個對象,則ThreadLocal也不能保證線程安全。
ThreadLocal原理
(上圖來源:https://blog.csdn.net/aaronsimon/article/details/82711336)??
- 每個Thread線程內部都有一個Map;
- Map里面存儲線程本地對象(key)和線程的變量副本(value)
- Thread內部的Map是由ThreadLocal維護的,由ThreadLocal負責向map獲取和設置線程的變量值。
所以對于不同的線程,每次獲取副本值時,別的線程并不能獲取到當前線程的副本值,形成了副本的隔離,互不干擾。
?
ThreadLocal源碼
??? 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();
??? }
? ? private T setInitialValue() {
? ? ? ? T value = initialValue();? //子類可以覆蓋的。
? ? ? ? Thread t = Thread.currentThread();
? ? ? ? ThreadLocalMap map = getMap(t);
? ? ? ? if (map != null)
? ? ? ? ? ? map.set(this, value);
? ? ? ? else
? ? ? ? ? ? createMap(t, value);
? ? ? ? return value;
? ? }
? ??
??? public void set(T value) {
??????? Thread t = Thread.currentThread();
??????? ThreadLocalMap map = getMap(t);
??????? if (map != null)
??????????? map.set(this, value);
??????? else
??????????? createMap(t, value);
??? }
?
??? ThreadLocalMap getMap(Thread t) {
??????? return t.threadLocals;
??? }
? void createMap(Thread t, T firstValue) {
??????? t.threadLocals = new ThreadLocalMap(this, firstValue);
??? }
?ThreadLocal.ThreadLocalMap threadLocals = null;
? void createMap(Thread t, T firstValue) {
??????? t.threadLocals = new ThreadLocalMap(this, firstValue);
??? }
?static class Entry extends WeakReference<ThreadLocal<?>> {
??????????? /** The value associated with this ThreadLocal. */
??????????? Object value;
??????????? Entry(ThreadLocal<?> k, Object v) {
??????????????? super(k);
??????????????? value = v;
??????????? }
??????? }
?
??? //每個ThreadLocal對象都有一個HashCode
??? private final int threadLocalHashCode = nextHashCode();
??? private static AtomicInteger nextHashCode =??????? new AtomicInteger();
??? private static final int HASH_INCREMENT = 0x61c88647;
??? private static int nextHashCode() {
??????? return nextHashCode.getAndAdd(HASH_INCREMENT);
??? }
?
Thread.ThreadLocalMap<ThreadLocal, Object>;
1、Thread: 當前線程,可以通過Thread.currentThread()獲取。
2、ThreadLocal:我們的static ThreadLocal變量。
3、Object:?當前線程共享變量。
我們調用ThreadLocal.get方法時,實際上是從當前線程中獲取ThreadLocalMap<ThreadLocal, Object>,然后根據當前ThreadLocal獲取當前線程共享變量Object。
ThreadLocal.set,ThreadLocal.remove實際上是同樣的道理。
?
這種存儲結構的好處:
1、線程死去的時候,線程共享變量ThreadLocalMap則銷毀。
2、ThreadLocalMap<ThreadLocal,Object>鍵值對數量為ThreadLocal的數量,一般來說ThreadLocal數量很少,相比在ThreadLocal中用Map<Thread, Object>鍵值對存儲線程共享變量(Thread數量一般來說比ThreadLocal數量多),性能提高很多。
?
關于ThreadLocalMap<ThreadLocal, Object>弱引用問題:
當線程沒有結束,但是ThreadLocal已經被回收,則可能導致線程中存在ThreadLocalMap<null, Object>的鍵值對,造成內存泄露。(ThreadLocal被回收,ThreadLocal關聯的線程共享變量還存在)。
雖然ThreadLocal的get,set方法可以清除ThreadLocalMap中key為null的value,但是get,set方法在內存泄露后并不會必然調用,所以為了防止此類情況的出現,有兩種手段。
1、使用完線程共享變量后,顯示調用ThreadLocalMap.remove方法清除線程共享變量;
2、JDK建議ThreadLocal定義為private static,這樣ThreadLocal的弱引用問題則不存在了。
InheritableThreadLocal
ThreadLocal大部分情況下均能正常work。但是,在當下互聯網環境下,經常會用到了異步方式來提高程序運行效率。當時當在主線程設置ThreadLocal變量,在子線程get?TheadLocal變量時,未能獲取到正確值。這是因為子線程與主線程不是同一個線程,因此獲取不到主線程設置的變量值。
JDK擴展了ThreadLocal,實現了一個子類InheritableThreadLocal,它能夠向子線程傳遞數據。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);} }?
InheritableTheadLocal主要復寫了getMap,createMap2個方法。操作的屬性為Thread的inheritableThreadLocals屬性。
ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;Thread類在構造時,會調用init方法
public Thread(Runnable target) {init(null, target, "Thread-" + nextThreadNum(), 0);}private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name;//當前線程為子線程的父線程Thread parent = currentThread();。。。。。。//調用ThreadLocal的createInheritedMap 方法if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/* Set thread ID */tid = nextThreadID();}createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);}private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {//獲取父線程的值,Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}}//這里是淺拷貝,與父值引用的是同一個引用,如果需要特殊處理,要覆蓋此方法。protected T childValue(T parentValue) {return parentValue;}上面解釋為什么 父線程的InheritableThreadLocal變量可以傳遞給子線程。但是父線程或者子線程再次通過set命令賦值,不會互相影響。因為關系的建立僅在初始化子線程時建立。
線程安全問題
ThreadLocal不能解決共享變量的線程安全問題,如果沒有子線程,則安全問題可以保證,但是如果有子線程,多個子線程引用的是同一個對象,如果都對此對象的屬性進行修改,則會導致線程安全問題。
ThreadLocal使用的正確姿勢
public static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = new InheritableThreadLocal<DateFormat>() {@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");} };每次綁定時,都會產生一個新的對象。
ThreadLocal局限性
ThreadLocal它并不能解決線程安全問題,它旨在用于傳遞數據。但是它能成功傳遞數據比如有個大前提:放數據和取數據的操作必須是處于相同線程。
InheritableThreadLocal,它能夠支持跨線程傳遞數據,但也僅限于父線程給子線程來傳遞數據。但是對于沒有任何關系的2個線程,它無能為力。
線程池搭配問題
由于線程池中是緩存使用過的線程,當線程被重復調用的時候并沒有再重新初始化init()線程,而是直接使用已經創建過的線程,所以值并不會被再次操作。因為實際的項目中線程池的使用頻率非常高,每一次從線程池中取出線程不能夠直接使用之前緩存的變量,所以要解決這一個問題,網上大部分是推薦使用alibaba的開源項目transmittable-thread-local。
?
TransmittableThreadLocal
JDK的InheritableThreadLocal類可以完成父線程到子線程的值傳遞。但對于使用線程池等會池化復用線程的執行組件的情況,線程由線程池創建好,并且線程是池化起來反復使用的;這時父子線程關系的ThreadLocal值傳遞已經沒有意義,應用需要的實際上是把 任務提交給線程池時的ThreadLocal值傳遞到 任務執行時。
參考官網:https://github.com/alibaba/transmittable-thread-local
源碼解析參考:https://www.cnblogs.com/hama1993/p/10409740.html
數據結構
?
數據流程
說明
通過包裝的ExecutorTtlWrapper提交Runnable時,一定不能是TtlRunnable,會拋出異常,只能是非TtlRunnalbe,會自動包裝,保證每次運行時都會獲取主線程的值。這樣才有可能在主線程值變更后可以獲取到。
?
?
?
?
?
?
總結
以上是生活随笔為你收集整理的ThreadLocal以及增强的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ConditionObject源码
- 下一篇: CountDownLatch应用及原理