Java並發編程原理與實戰二十五:ThreadLocal線程局部變量的使用和原理
1.什麽是ThreadLocal
ThreadLocal顧名思義是線程局部變量。這種變量和普通的變量不同,這種變量在每個線程中通過get和set方法訪問, 每個線程有自己獨立的變量副本。線程局部變量不存在多個線程同時對同一個變量的操作,所以不會有線程安全問題。
2.ThreadLocal變量的使用
public class ThreadLocalDemo { private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){ // 初始化局部變量的值@Override protected Integer initialValue() { return new Integer(0); } }; public Integer getNext() { Integer value = threadLocal.get(); value++; threadLocal.set(value); return value; } public static voidmain(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(5); final ThreadLocalDemo demo = new ThreadLocalDemo(); executor.execute(new Runnable() { @Override public void run() { while (true) { System.out.println(Thread.currentThread().getName()+ ": " + demo.getNext()); } } }); executor.execute(new Runnable() { @Override public void run() { while (true) { System.out.println(Thread.currentThread().getName() + ": " + demo.getNext()); } } }); } }
3.跳出ThreadLocal誤區
ThreadLocal一般稱為線程本地變量,它是一種特殊的線程綁定機制,將變量與線程綁定在一起,為每一個線程維護一個獨立的變量副本。通過ThreadLocal可以將對象的可見範圍限制在同一個線程內。
跳出誤區
需要重點強調的的是,不要拿ThreadLocal和synchronized做類比,因為這種比較壓根就是無意義的!sysnchronized是一種互斥同步機制,是為了保證在多線程環境下對於共享資源的正確訪問。而ThreadLocal從本質上講,無非是提供了一個“線程級”的變量作用域,它是一種線程封閉(每個線程獨享變量)技術,更直白點講,ThreadLocal可以理解為將對象的作用範圍限制在一個線程上下文中,使得變量的作用域為“線程級”。
沒有ThreadLocal的時候,一個線程在其聲明周期內,可能穿過多個層級,多個方法,如果有個對象需要在此線程周期內多次調用,且是跨層級的(線程內共享),通常的做法是通過參數進行傳遞;而ThreadLocal將變量綁定在線程上,在一個線程周期內,無論“你身處何地”,只需通過其提供的get方法就可輕松獲取到對象。極大地提高了對於“線程級變量”的訪問便利性。
假設我們要為每個線程關聯一個唯一的序號,在每個線程周期內,我們需要多次訪問這個序號,這時我們就可以使用ThreadLocal了.(當然下面這個例子沒有完全體現出跨層級跨方法的調用,理解就可以了)
package concurrent; import java.util.concurrent.atomic.AtomicInteger; /** * Created by chengxiao on 2016/12/12. */ public class ThreadLocalDemo { public static void main(String []args){ for(int i=0;i<5;i++){ final Thread t = new Thread(){ @Override public void run(){ System.out.println("當前線程:"+Thread.currentThread().getName()+",已分配ID:"+ThreadId.get()); } }; t.start(); } } static class ThreadId{ //一個遞增的序列,使用AtomicInger原子變量保證線程安全 private static final AtomicInteger nextId = new AtomicInteger(0); //線程本地變量,為每個線程關聯一個唯一的序號 private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return nextId.getAndIncrement();//相當於nextId++,由於nextId++這種操作是個復合操作而非原子操作,會有線程安全問題(可能在初始化時就獲取到相同的ID,所以使用原子變量 } }; //返回當前線程的唯一的序列,如果第一次get,會先調用initialValue,後面看源碼就了解了 public static int get() { return threadId.get(); } } }
執行結果,可以看到每個線程都分配到了一個唯一的ID,同時在此線程範圍內的"任何地點",我們都可以通過ThreadId.get()這種方式直接獲取。
當前線程:Thread-4,已分配ID:1 當前線程:Thread-0,已分配ID:0 當前線程:Thread-2,已分配ID:3 當前線程:Thread-1,已分配ID:4 當前線程:Thread-3,已分配ID:2
4.ThreadLocal原理
set操作,為線程綁定變量
public void set(T value) { Thread t = Thread.currentThread();//1.首先獲取當前線程對象 ThreadLocalMap map = getMap(t);//2.獲取該線程對象的ThreadLocalMap if (map != null) map.set(this, value);//如果map不為空,執行set操作,以當前threadLocal對象為key,實際存儲對象為value進行set操作 else createMap(t, value);//如果map為空,則為該線程創建ThreadLocalMap }
可以看到,ThreadLocal不過是個入口,真正的變量是綁定在線程上的。
ThreadLocalMap getMap(Thread t) { return t.threadLocals;//線程對象持有ThreadLocalMap的引用 }
下面給是Thread類中的定義,每個線程對象都擁有一個ThreadLocalMap對象
ThreadLocal.ThreadLocalMap threadLocals = null;
現在,我們能看出ThreadLocal的設計思想了:
1.ThreadLocal僅僅是個變量訪問的入口;
2.每一個Thread對象都有一個ThreadLocalMap對象,這個ThreadLocalMap持有對象的引用;
3.ThreadLocalMap以當前的threadlocal對象為key,以真正的存儲對象為value。get時通過threadlocal實例就可以找到綁定在當前線程上的對象。
乍看上去,這種設計確實有些繞。我們完全可以在設計成Map<Thread,T>這種形式,一個線程對應一個存儲對象。ThreadLocal這樣設計的目的主要有兩個:
一是可以保證當前線程結束時相關對象能盡快被回收;二是ThreadLocalMap中的元素會大大減少,我們都知道map過大更容易造成哈希沖突而導致性能變差。
我們再來看看get方法
public T get() { Thread t = Thread.currentThread();//1.首先獲取當前線程 ThreadLocalMap map = getMap(t);//2.獲取線程的map對象 if (map != null) {//3.如果map不為空,以threadlocal實例為key獲取到對應Entry,然後從Entry中取出對象即可。 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue();//如果map為空,也就是第一次沒有調用set直接get(或者調用過set,又調用了remove)時,為其設定初始值 }
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; }
initialValue方法,默認是null,訪問權限是protected,即允許重寫。
protected T initialValue() { return null; }
談到這兒,我們應該已經對ThreadLocal的設計目的及設計思想有一定的了解了。
5.線程獨享變量
還有一個會引起疑惑的問題,我們說ThreadLocal為每一個線程維護一個獨立的變量副本,那麽是不是說各個線程之間真正的做到對於對象的“完全自治”而不對其他線程的對象產生影響呢?其實這已經不屬於對於ThreadLocal的討論,而是你出於何種目的去使用ThreadLocal。如果我們為一個線程關聯的對象是“完全獨享”的,也就是每個線程擁有一整套的新的 棧中的對象引用+堆中的對象,那麽這種情況下是真正的徹底的“線程獨享變量”,相當於一種深度拷貝,每個線程自己玩自己的,對該對象做任何的操作也不會對別的線程有任何影響。
另一種更普遍的情況,所謂的獨享變量副本,其實也就是每個線程都擁有一個獨立的對象引用,而堆中的對象還是線程間共享的,這種情況下,自然還是會涉及到對共享資源的訪問操作,依然會有線程不安全的風險。所以說,ThreadLocal無法解決線程安全問題。
所以,需不需要完全獨享變量,進行完全隔離,就取決於你的應用場景了。可以想象,對象過大的時候,如果每個線程都有這麽一份“深拷貝”,並發又比較大,對於服務器的壓力自然是很大的。像web開發中的servlet,servlet是線程不安全的,一請求一線程,多個線程共享一個servlet對象;而早期的CGI設計中,N個請求就對應N個對象,並發量大了之後性能自然就很差。
ThreadLocal在spring的事務管理,包括Hibernate的session管理等都有出現,在web開發中,有時會用來管理用戶會話 HttpSession,
web交互中這種典型的一請求一線程的場景似乎比較適合使用ThreadLocal,但是需要特別註意的是,由於此時session與線程關聯,而tomcat這些web服務器多會采用線程池機制,
也就是說線程是可復用的,所以在每一次進入的時候都需要重新進行set,或者在結束時及時remove
ThreadLocal線程安全案例
參考資料:
《java並發編程實戰》龍果學院
Java並發編程原理與實戰二十五:ThreadLocal線程局部變量的使用和原理