為什麼要寫這篇文章
網上很多文章都在講ThreadLocal的意義所在,然後大部分都在說ThreadLocal是為瞭解決安全執行緒而生的,旨在解決並發安全問題,這種說法是片面的,導致很多人理解不到ThreadLocal真正用途 ThreadLocal是什麼
ThreadLocal翻譯過來是線程局部變數,而不是本地線程。
ThreadLocal是為瞭解決在一個線程中,某個或者某些資源在不同層次的代碼中通過參數來回傳遞的問題,但是要在沒有增加效能損耗的情況下,保證安全執行緒。 為什麼要用ThreadLocal 單資源單安全執行緒
比如我們有兩個模組,一個把資源(靜態變數)增加100,一個把資源減少100,但是我們不想把資源通過參數傳入,只能把這個資源設定為靜態變數。
單線程執行個體代碼:
import java.util.concurrent.atomic.AtomicInteger;public class IncThread implements Runnable{ private static int VALUE = 0; @Override public void run() { iscr100();//模組1, 增加100 descr100();//模組2, 減少100 } public void descr100(){ for(int i = 0; i< 1000; i++){ VALUE--; try { //類比IO,讓CPC進行切換 Thread.sleep(1); } catch (InterruptedException e) { } } } public void iscr100(){ for(int i = 0; i< 1000; i++){ VALUE++; try { //類比IO,讓CPC進行切換 Thread.sleep(1); } catch (InterruptedException e) { } } } public static void main(String[] args) throws InterruptedException { IncThread myInc = new IncThread(); Thread thread = new Thread(myInc); thread.start(); //保證主線程退出時,其他線程也執行完了 Thread.sleep(10000); //列印最終結果 System.out.println(VALUE); }}
單線程下沒有問題,上面的樣本VALUE的最終結果肯定是0。 單資源多線程不安全
但是如果是多線程下會正確麼。 我們修改main方法,改用兩個線程執行:
public static void main(String[] args) throws InterruptedException { IncThread myInc1 = new IncThread(); Thread thread1 = new Thread(myInc1); Thread thread2 = new Thread(myInc1); thread1.start(); thread2.start(); //保證主線程退出時,其他線程也執行完了 Thread.sleep(10000); //列印最終結果 System.out.println(VALUE); }
執行的結果肯定不是0,因為int的++和–操作不是安全執行緒的。
當然為瞭解決這個問題,我們可以把VALUE設定成安全執行緒的類,比如AtomicInteger,這針對於資源是唯一的情況下可以這樣做,雖然這麼做有一定的效能損耗,這個是沒有辦法的,但是如果是多個資源,每個線程都可以分配到一個資源的情況下,使用安全執行緒類會降低效能,再說,萬一我們的資源不是安全執行緒的呢,比如資料庫連接。 多資源多線程怎麼安全
這個時候ThreadLocal就登上曆史舞台了。
import java.util.concurrent.atomic.AtomicInteger;public class IncThread implements Runnable{ private static ThreadLocal<Integer> VALUE = new ThreadLocal<>(); @Override public void run() { VALUE.set(0);//初始化資源,比如擷取資料庫連接池 iscr100();//模組1, 增加100 descr100();//模組2, 減少100 System.out.println(VALUE.get()); } public void descr100(){ for(int i = 0; i< 1000; i++){ try { Integer integer = VALUE.get(); integer++; //類比IO,讓CPC進行切換 Thread.sleep(1); } catch (InterruptedException e) { } } } public void iscr100(){ for(int i = 0; i< 1000; i++){ Integer integer = VALUE.get(); integer--; try { //類比IO,讓CPC進行切換 Thread.sleep(1); } catch (InterruptedException e) { } } } public static void main(String[] args) throws InterruptedException { IncThread myInc1 = new IncThread(); Thread thread1 = new Thread(myInc1); Thread thread2 = new Thread(myInc1); thread1.start(); thread2.start(); //保證主線程退出時,其他線程也執行完了 Thread.sleep(10000); }}
到此為止,我們講解了ThreadLocal的意義所在,不過有人會說了,像這種多個資源的同步的情況下,我們可以自己實現呀,比如使用Map,以線程名稱作為KEY,以資源作為VALUE,提出這個方案的人員是很聰明的,ThreadLocal也確實是使用Map作為儲存的,但是這個方案有兩個壞處。
1. 通用性差: 每個架構的線程名稱是不定的,比如Spring架構、Servlet等等,很難,也是沒辦法有一個通用的Map能實現這個功能。
2. 易用性低:我們自己實現一套機制,很難使用到別人已有的架構中,只能在我們自己的架構裡使用。
那麼,我們就不得不選擇ThreadLocal了。 ThreadLocal源碼分析
既然ThreadLocal是為瞭解決多資源多線程下資源到處傳遞的問題,那麼ThreadLocal至少要有set, get和delete這個三個方法,實際上ThreadLocal有5個方法:
| 描述符和傳回值 |
方法名 |
描述 |
| T |
get() |
Returns the value in the current thread’s copy of this thread-local variable. |
| protected T |
initialValue() |
Returns the current thread’s “initial value” for this thread-local variable. |
| void |
remove() |
Removes the current thread’s value for this thread-local variable. |
| void |
set(T value) |
Sets the current thread’s copy of this thread-local variable to the specified value. |
| static <S> ThreadLocal<S> |
withInitial(Supplier<? extends S> supplier) |
Creates a thread local variable. |
Set方法
public void set(T value) { //擷取當前線程的索引 Thread t = Thread.currentThread(); //擷取當前線程的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) //設定值 map.set(this, value); else //第一次設定值 createMap(t, value); }
可以看出,ThreadLocal能實現跨代碼層次擷取線程資源的根本原因是有了Thread.currentThread() 這個靜態方法,這樣ThreadLocal可以隨時隨地做任何操作,而不需要傳入線程名稱或者ID。
我們看下getMap(t)做了什麼事情。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
而Thread類有一個成員變數:
ThreadLocal.ThreadLocalMap threadLocals = null;
那麼我們就能理解到,ThreadLocal儲存的資源實際上儲存到了Thread上,ThreadLocal只是作為一個能找到這個資源的索引。 get()方法
我們知道了ThreadLocal執行個體是作為當前資源的索引,那麼get()方法的源碼就應運而生了。
public T get() { Thread t = Thread.currentThread(); //擷取到當前線程的ThreadLocalMap ThreadLocalMap map = getMap(t); //map存在時 if (map != null) { //擷取到資源 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //map不存在或者沒有set資源時,返回初始化的值 return setInitialValue(); }
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; }
如果get資源時不存在,則最終會調用initialValue()方法,初始化資源:
protected T initialValue() { return null; }
但是這個方法會最終返回null,所以如果我們set資源時,如果調用get會直接返回null。 withInitial() 靜態方法
為了不get到null,避免我們還要判斷是否為null時再初始化ThreadLocal,ThreadLocal提供給我們一個靜態方法,可以傳入一個function來初始化這個ThreadLocal。
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); }
remove()方法
remove()就比較簡單了,這裡就不介紹了。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) // 當前線程的ThreadLocalMap存在時刪除此資源。 m.remove(this); }
最後
最後我們通過官方的解釋來總結一下ThreadLocal:
ThreadLocal提供了線程局部變數,這些變數和普通的變數是不一樣的,因為每一個線程的ThreadLocal都有自己的副本,並且是獨立初始化的副本,其他線程是無法訪問到的。使用ThreadLocal通常是與線程關聯類別中的私人靜態欄位,比如使用者Id和事務Id等。