【Java多線程】之ThreadLocal分析

1.對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進行了修改。
到這裏,可能會有這樣的考慮,既然不需要在線程之間共享這個變量,可以直接這樣處理,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫連接,然後在方法調用完畢在釋放這個連接,比如下面這樣:
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();
    }
}

這樣處理確實也沒有任何問題,由於每次都是在方法內部創建的連接,那麼線程之間自然不存在線程安全問題。但是這樣會有一個致命影響:導致服務器壓力非常大,並且嚴重影響程序執行性能。由於在方法中需要頻繁地開啓和關閉數據庫連接,這樣不僅嚴重影響程序執行效率,還可導致服務器壓力巨大。
這種情況下使用ThreadLocal是再適合不過了,因爲ThreadLocal在每個線程中對該變量會創建一個副本,即每個線程內部都會有一個該變量,且在線程內部都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能
但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由於在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的佔用會不不使用ThreadLocal要大

2.深入理解ThreadLocal類

現在來看看ThreadLocal是如何實現的
先了解一下ThreadLocal類提供的幾個方法
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
get()方法是用來獲取ThreadLocal在當前線程中保存的副本變量,set()用來設置當前線程中變量的副本,remove()用來移除當前線程中變量的副本,initialValue()是一個protected方法,一般用來在使用時進行重寫,他是一個延時加載方法,下面會詳細說明:
首先看一下ThreadLocal類是如何爲每個線程創建一個變量的副本的
先看下get方法的實現:
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();
}

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

如果獲取成功,則返回value值
如果map爲空,則調用setInitialValue方法返回value。
我們來具體分析下getMap方法中做了什麼:
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
其實就是返回當前線程t中的一個成員變量threadLocals,我們繼續看下變量threadLocals是什麼:
ThreadLocal.ThreadLocalMap threadLocals = null;

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

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}
可以看到ThreadLocalMap的Entry繼承了WeakReference,並且使用ThreadLocal作爲鍵值。
繼續看看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;
}
很容易理解,就是如果map不爲空,就設置鍵值,爲空,就創建Map,看一個createMap的實現:
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
至此,差不多就明白了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了。

3.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();
}
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;
}

4.ThreadLocal會不會造成內存泄露?
    爲什麼這樣認爲:(ThreadLocalMap是使用ThreadLocal的弱引用作爲Key的。如果一個ThreadLocal沒有外部強引用引用他,那麼在系統GC的時候,這個對象會被回收,這樣一來,ThreadLocalMap中的鍵就爲null, 無法訪問,如果當前線程遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈。永遠無法回收)
    但其實並不會:整理出ThreadLocalMap的getEntry函數的流程:
  1.  首先從ThreadLocal的直接索引位置(位置通過ThreadLocal.threadLocalHashCode & (len-1))獲得Entry e,如果e不爲null並且key相同則返回e。
  2.  如果e爲null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry,另外,如果key值爲null,則擦除該位置的Entry,否則繼續向下一個位置查詢。
    由上可以看出,在這個過程中遇到的key爲null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,自然會被回收。set也有類型的思想。但是這個思路必須在調用set和get函數的前提下才能實現,所以很多情況下需要手動調用remove函數,手動刪除不需要的TreadLocal。
    JDK建議將ThreadLocal變量定義爲private static的,這樣的話ThreadLocal的生命週期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal就不會回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove,防止內存泄露 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章