ThreadLocal用法和實現原理


 

如果你定義了一個單實例的java bean,它有若干屬性,但是有一個屬性不是線程安全的,比如說HashMap。並且碰巧你並不需要在不同的線程中共享這個屬性,也就是說這個屬性不存在跨線程的意義。那么你不要sychronize這么復雜的東西,ThreadLocal將是你不錯的選擇。

舉例來說:

import java.util.HashMap;

public class TreadLocalTest {

    static ThreadLocal<HashMap> map0 = new ThreadLocal<HashMap>(){ 
        @Override 
        protected HashMap initialValue() { 
            System.out.println(Thread.currentThread().getName()+"initialValue"); 
            return new HashMap(); 
        } 
    }; 
    public void run(){ 
        Thread[] runs = new Thread[3]; 
        for(int i=0;i<runs.length;i++){ 
            runs[i]=new Thread(new T1(i)); 
        } 
        for(int i=0;i<runs.length;i++){ 
            runs[i].start(); 
        } 
    } 
    public static class T1 implements Runnable{ 
        int id; 
        public T1(int id0){ 
            id = id0; 
        } 
        public void run() { 
            System.out.println(Thread.currentThread().getName()+":start"); 
            HashMap map = map0.get(); 
            for(int i=0;i<10;i++){ 
                map.put(i, i+id*100); 
                try{ 
                    Thread.sleep(100); 
                }catch(Exception ex){ 
                } 
            } 
            System.out.println(Thread.currentThread().getName()+':'+map); 
        } 
    } 
    /** 
     * Main 
     * @param args 
     */ 
    public static void main(String[] args){ 
        TreadLocalTest test = new TreadLocalTest(); 
        test.run(); 
    }

}

 

輸出解釋;

Thread-1:start 
Thread-2:start 
Thread-0:start 
Thread-2initialValue 
Thread-1initialValue 
Thread-0initialValue 
Thread-1:{0=100, 1=101, 2=102, 3=103, 4=104, 5=105, 6=106, 7=107, 8=108, 9=109} 
Thread-2:{0=200, 1=201, 2=202, 3=203, 4=204, 5=205, 6=206, 7=207, 8=208, 9=209} 
Thread-0:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}

可以看到map0 雖然是個靜態變量,但是initialValue被調用了三次,通過debug發現,initialValue是從map0.get處發起的。而且每個線程都有自己的map,雖然他們同時執行。

進入Theadlocal代碼,可以發現如下的片段;

public T get() { 
        Thread t = Thread.currentThread(); 
        ThreadLocalMap map = getMap(t); 
        if (map != null) { 
            ThreadLocalMap.Entry e = map.getEntry(this); 
            if (e != null) 
                return (T)e.value; 
        } 
        return setInitialValue(); 
    }

 

這說明ThreadLocal確實只有一個變量,但是它內部包含一個map,針對每個thread保留一個entry,如果對應的thread不存在則會調用initialValue。

一.對ThreadLocal的理解

  ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多。可能很多朋友都知道ThreadLocal為變量在每個線程中都創建了一個副本,那么每個線程可以訪問自己內部的副本變量。

  這句話從字面上看起來很容易理解,但是真正理解並不是那么容易。

  我們還是先來看一個例子:

class ConnectionManager {
     
    private static Connection connect = null;
     
    public static Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public static void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}

 

   假設有這樣一個數據庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的,但是如果在多線程中使用呢?很顯然,在多線程中使用會存在線程安全問題:第一,這里面的2個方法都沒有進行同步,很可能在openConnection方法中會多次創建connect;第二,由於connect是共享變量,那么必然在調用connect的地方需要使用到同步來保障線程安全,因為很可能一個線程在使用connect進行數據庫操作,而另外一個線程調用closeConnection關閉鏈接。

  所以出於線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,並且在調用connect的地方需要進行同步處理。

  這樣將會大大影響程序執行效率,因為一個線程在使用connect進行數據庫操作的時候,其他線程只有等待。

  那么大家來仔細分析一下這個問題,這地方到底需不需要將connect變量進行共享?事實上,是不需要的。假如每個線程中都有一個connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關系的,即一個線程不需要關心其他線程是否對這個connect進行了修改的。

  到這里,可能會有朋友想到,既然不需要在線程之間共享這個變量,可以直接這樣處理,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫鏈接,然后在方法調用完畢再釋放這個連接。比如下面這樣:

 
 

   這樣處理確實也沒有任何問題,由於每次都是在方法內部創建的連接,那么線程之間自然不存在線程安全問題。但是這樣會有一個致命的影響:導致服務器壓力非常大,並且嚴重影響程序執行性能。由於在方法中需要頻繁地開啟和關閉數據庫連接,這樣不盡嚴重影響程序執行效率,還可能導致服務器壓力巨大。

  那么這種情況下使用ThreadLocal是再適合不過的了,因為ThreadLocal在每個線程中對該變量會創建一個副本,即每個線程內部都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。

  但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由於在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的占用會比不使用ThreadLocal要大。

二.深入解析ThreadLocal類

  在上面談到了對ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實現的。

  先了解一下ThreadLocal類提供的幾個方法:

class ConnectionManager {
     
    private  Connection connect = null;
     
    public Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}
 
 
class Dao{
    public void insert() {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.openConnection();
         
        //使用connection進行操作
         
        connectionManager.closeConnection();
    }
}

 

   get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本,set()用來設置當前線程中變量的副本,remove()用來移除當前線程中變量的副本,initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法,下面會詳細說明。

  首先我們來看一下ThreadLocal類是如何為每個線程創建一個變量的副本的。

  先看下get方法的實現:

  

   第一句是取得當前線程,然后通過getMap(t)方法獲取到一個map,map的類型為ThreadLocalMap。然后接着下面獲取到<key,value>鍵值對,注意這里獲取鍵值對傳進去的是  this,而不是當前線程t。

  如果獲取成功,則返回value值。

  如果map為空,則調用setInitialValue方法返回value。

  我們上面的每一句來仔細分析:

  首先看一下getMap方法中做了什么:

  

  可能大家沒有想到的是,在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals。

  那么我們繼續取Thread類中取看一下成員變量threadLocals是什么:

  

  實際上就是一個ThreadLocalMap,這個類型是ThreadLocal類的一個內部類,我們繼續取看ThreadLocalMap的實現:

  

  可以看到ThreadLocalMap的Entry繼承了WeakReference,並且使用ThreadLocal作為鍵值。

  然后再繼續看setInitialValue方法的具體實現:

  很容易了解,就是如果map不為空,就設置鍵值對,為空,再創建Map,看一下createMap的實現:

  

  至此,可能大部分朋友已經明白了ThreadLocal是如何為每個線程創建變量的副本的:

  首先,在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當前ThreadLocal變量,value為變量副本(即T類型的變量)。

  初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。

  然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。

  下面通過一個例子來證明通過ThreadLocal能達到在每個線程中創建變量副本的效果:

public class Test {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();
 
     
    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }
     
    public long getLong() {
        return longLocal.get();
    }
     
    public String getString() {
        return stringLocal.get();
    }
     
    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();
         
         
        test.set();
        System.out.println(test.getLong());
        System.out.println(test.getString());
     
         
        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();
         
        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}

 

   這段代碼的輸出結果為:

  

  從這段代碼的輸出結果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣。最后一次在main線程再次打印副本值是為了證明在main線程中和thread1線程中的副本值確實是不同的。

  總結一下:

  1)實際的通過ThreadLocal創建的副本是存儲在每個線程自己的threadLocals中的;

  2)為何threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對象,因為每個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;

  3)在進行get之前,必須先set,否則會報空指針異常;

      如果想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法。

    因為在上面的代碼分析過程中,我們發現如果沒有先set的話,即在map中查找不到對應的存儲,則會通過調用setInitialValue方法返回i,而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認情況下,initialValue方法返回的是null。

  

  看下面這個例子:

  

public class Test {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();
 
    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }
     
    public long getLong() {
        return longLocal.get();
    }
     
    public String getString() {
        return stringLocal.get();
    }
     
    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();
         
        System.out.println(test.getLong());
        System.out.println(test.getString());
 
        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();
         
        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}

 

   在main線程中,沒有先set,直接get的話,運行時會報空指針異常。

  但是如果改成下面這段代碼,即重寫了initialValue方法:

public class Test {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
        protected Long initialValue() {
            return Thread.currentThread().getId();
        };
    };
    ThreadLocal<String> stringLocal = new ThreadLocal<String>(){;
        protected String initialValue() {
            return Thread.currentThread().getName();
        };
    };
 
     
    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }
     
    public long getLong() {
        return longLocal.get();
    }
     
    public String getString() {
        return stringLocal.get();
    }
     
    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();
 
        test.set();
        System.out.println(test.getLong());
        System.out.println(test.getString());
     
         
        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();
         
        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}

 

   就可以直接不用先set而直接調用get了。

三.ThreadLocal的應用場景

  最常見的ThreadLocal使用場景為 用來解決 數據庫連接、Session管理等。

  如:

private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
    return DriverManager.getConnection(DB_URL);
}
};
 
public static Connection getConnection() {
return connectionHolder.get();
}

 

   下面這段代碼摘自:

  http://www.iteye.com/topic/103804

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虛擬機》

  《Java編程思想》

  http://ifeve.com/thread-management-10/

  http://www.ibm.com/developerworks/cn/java/j-threads/index3.html

  http://www.iteye.com/topic/103804

  http://www.iteye.com/topic/777716

  http://www.iteye.com/topic/757478

  http://blog.csdn.net/ghsau/article/details/15732053

  http://ispring.iteye.com/blog/162982

  http://blog.csdn.net/imzoer/article/details/8262101

  http://www.blogjava.net/wumi9527/archive/2010/09/10/331654.html

  http://bbs.csdn.net/topics/380049261


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM