標籤:cond ace 處理 nal 結構 read 通過 初始 隨機
一、ThreadLocal簡介
ThreadLocal是線程的局部變數,是每一個線程所單獨持有的,其他線程不能對其進行訪問,通常是類中的private static欄位。
我們知道有時候一個對象的變數會被多個線程所訪問,這時就會有安全執行緒問題,當然我們可以使用synchorinized 關鍵字來為此變數加鎖,進行同步處理,從而限制只能有一個線程來使用此變數,但是加鎖會大大影響程式執行效率,此外我們還可以使用ThreadLocal來解決對某一個變數的存取違規問題。
當使用ThreadLocal維護變數的時候 為每一個使用該變數的線程提供一個獨立的變數副本,即每個線程內部都會有一個該變數,這樣同時多個線程訪問該變數並不會彼此相互影響,因此他們使用的都是自己從記憶體中拷貝過來的變數的副本, 這樣就不存線上程安全問題,也不會影響程式的執行效能。
但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由於在每個線程中都建立了副本,所以要考慮它對資源的消耗,比如記憶體的佔用會比不使用ThreadLocal要大。
二、ThreadLocal源碼分析(1)ThreadLocal方法
ThreadLocal 的幾個方法: ThreadLocal 可以儲存任何類型的變數對象, get返回的是一個Object對象,但是我們可以通過泛型來制定儲存物件的類型。
public T get() { } // 用來擷取ThreadLocal在當前線程中儲存的變數副本public void set(T value) { } //set()用來設定當前線程中變數的副本public void remove() { } //remove()用來移除當前線程中變數的副本protected T initialValue() { } //initialValue()是一個protected方法,一般是用來在使用時進行重寫的
(2)ThreadLocal實現原理1.內部結構
Thread 在內部是通過ThreadLocalMap來維護ThreadLocal變數表, 在Thread類中有一個threadLocals 變數,是ThreadLocalMap類型的,它就是為每一個線程來儲存自身的ThreadLocal變數的。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 是定義在ThreadLocal 類裡的內部類,它的作用是儲存線程的局部變數。ThreadLocalMap 以ThreadLocal的引用作為鍵,以局部變數作為值,儲存在ThreadLocalMap.Entry (一種儲存索引值的資料結構)裡。這是因為在每一個線程裡面,可能存在著多個ThreadLocal變數
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }}
2.調用過程
初始時,在Thread裡面,threadLocals為空白,當通過ThreadLocal變數調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變數為索引值,以ThreadLocal要儲存的副本變數為value,存到threadLocals。
然後在當前線程裡面,如果要使用副本變數,就可以通過get方法在threadLocals裡面尋找
注意:
在進行get之前,必須先set,否則會報null 指標異常;
如果想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法。
ThreadLocal的set(T value)方法
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);}
內部類ThreadLocalMap的set(ThreadLocal<?> key,Object value)
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; // Hash 定址,與table數組長度減1(二進位全是1)相與,所以數組長度必須為2的次方,減小hash重複的可能性 int i = key.threadLocalHashCode & (len-1); //從hash值計算出的下標開始遍曆 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //獲得該Entry的鍵 ThreadLocal<?> k = e.get(); //如果鍵和傳過來的相同,覆蓋原值,也說明,一個ThreadLocal變數只能為一個線程儲存一個局部變數 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();}
可以看出ThreadLocalMap 採用線性探測再散列解決Hash衝突的問題。即,如果一次Hash計算出來的數組下標被佔用,即hash值重複了,則在該下標的基礎上加1測試下一個下標,直到找到空值。比如說,Hash計算出來下標i為6,table[6] 已經有值了,那麼就嘗試table[7]是否被佔用,依次類推,直到找到空值。以上,就是儲存執行緒區域變數的方法。
TheadLocal的get()方法
public T get() { //獲得當前線程 Thread t = Thread.currentThread(); //得到當前線程的一個threadLocals 變數ThreadLocalMap map = getMap(t);if (map != null) { // 如果不為空白,以當前ThreadLocal為主鍵獲得對應的Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; }} //如果值為空白,則進行初始化return setInitialValue();}
ThreadLocal的setInitialValue()方法
private T setInitialValue() { //獲得初始預設值T value = initialValue(); //得到當前線程Thread t = Thread.currentThread(); // 獲得該線程的ThreadLocalMap引用ThreadLocalMap map = getMap(t); //不為空白則覆蓋if (map != null) map.set(this, value);else //若是為空白,則進行初始化,鍵為本ThreadLocal變數,值為預設值 createMap(t, value);}// 預設初始化返回null值,這也是 下面demo 為什麼需要重寫該方法的原因。如果沒有重寫,第一次get()操作獲得的執行緒區域變數為null,需要進行判斷並手動調用set()進行初始化protected T initialValue() { return null;}
三、Demo
下面是個ThreadLocal使用的執行個體,兩個任務共用同一個變數,並且兩個任務都把該變數設定為了線程私人變數,這樣,雖然兩個任務都”持有“同一變數,但各自持有該變數的拷貝。因此,當一個線程修改該變數時,不會影響另一線程該變數的值。
package thread.ThreadLocalTest;import java.util.Random;import java.util.concurrent.TimeUnit;/*** Created by StoneGeek on 2018/8/1.* 部落格地址:http://www.cnblogs.com/sxkgeek*/public class ThreadLocalDemo2 implements Runnable { // 一般會把 ThreadLocal 設定為static 。它只是個為線程設定局部變數的入口,多個線程只需要一個入口 private static ThreadLocal<Student> localStudent = new ThreadLocal() { // 一般會重寫初始化方法,一會分析源碼時候會解釋為什麼 @Override public Student initialValue() { return new Student(); }};private Student student = null;@Overridepublic void run() { String threadName = Thread.currentThread().getName(); System.out.println("【" + threadName + "】:is running !"); Random ramdom = new Random(); //隨機產生一個變數 int age = ramdom.nextInt(100); System.out.println("【" + threadName + "】:set age to :" + age); // 獲得線程局部變數,改變屬性值 Student stu = getStudent(); stu.setAge(age); System.out.println("【" + threadName + "】:第一次讀到的age值為 :" + stu.getAge()); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("【" + threadName + "】:第二次讀到的age值為 :" + stu.getAge());}public Student getStudent() { student = localStudent.get();// 如果不重寫初始化方法,則需要判斷是否為空白,然後手動為ThreadLocal賦值,否則的話會報null 指標異常// if(student == null){// student = new Student();// localStudent.set(student);// } return student;}public static void main(String[] args) { ThreadLocalDemo2 ll = new ThreadLocalDemo2(); Thread t1 = new Thread(ll, "線程1"); Thread t2 = new Thread(ll, "線程2"); t1.start(); t2.start();}}console列印:【線程2】:is running !【線程1】:is running !【線程1】:set age to :67【線程2】:set age to :4【線程1】:第一次讀到的age值為 :67【線程2】:第一次讀到的age值為 :4【線程1】:第二次讀到的age值為 :67【線程2】:第二次讀到的age值為 :4
四、應用情境
最常見的ThreadLocal使用情境為
用來解決 資料庫連接、Session管理等。
資料庫連接
Class A implements Runnable{private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { return DriverManager.getConnection(DB_URL); }};public static Connection getConnection() { return connectionHolder.get();}}
Session管理
private static final ThreadLocal threadSession = new ThreadLocal();public static Session getSession() throws InfrastructureException { Session s = (Session) threadSession.get(); try { if (s == null) { s = getSessionFactory().openSession(); threadSession.set(s); } } catch (HibernateException ex) { throw new InfrastructureException(ex); } return s;}
java之ThreadLocal詳解