ThreadLocal是什麼?一篇博客幫你搞定。

1. ThreadLocal是什麼?

首先,學過操作系統的都應該知道,同一個進程中的線程之間的頭是相互獨立的,數據部分是共享的。現在如果我們想讓每個線程都有自己的數據,從而實現線程數據隔壁,那麼如何實現?ThreadLocal就是來做這個事情的,通過ThreadLocal可以給線程設置自己的局部變量,也可取出自己的局部變量。說白了,ThreadLocal就是想在多線程環境下去保證成員變量的安全。

另外,通過ThreadLocal的字面意思(線程的局部變量)也可以初步瞭解其作用。

注意:本文中說的線程的局部變量是指線程自己的數據,不是指方法中的局部變量。

2. ThreadLocal如何實現對線程局部變量的操作

2.1 分析

首先,我們查看一下ThreadLocal類中的源碼:
在這裏插入圖片描述
以下是ThreadLocal類中的部分源碼:

public class ThreadLocal<T> {

	ThreadLocal.ThreadLocalMap threadLocals = null;

	public T get() {
		//得到當前的線程對象
        Thread t = Thread.currentThread();
        
        //獲取該線程的屬性:ThreadLocalMap;ThreadLocalMap是用來存放該線程所有的局部變量的容器,是一個map結構
        ThreadLocalMap map = getMap(t);
        
        //如果map存在,則將當前ThreadLocal對象作爲key,根據key獲取內容
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

	public void set(T value) {
		//得到當前的線程對象
        Thread t = Thread.currentThread();
		
		//獲取該線程的屬性:ThreadLocalMap
        ThreadLocalMap map = getMap(t);

		//如果map存在,則以當前ThreadLocal對象爲key,在map中設置key-value
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

	//獲取該線程的成員變量:threadLocals
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

	//ThreadLocalMap類,是ThreadLocal的內部類
	static class ThreadLocalMap {
	
		// Entry類,是ThreadLocalMap的內部類
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

		private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
				...
            tab[i] = new Entry(key, value);
            	...
        }
     }
   }

注意:如果以上源碼看不懂沒關係,接着往下看,我詳細解釋。


	public void set(T value) {
		//得到當前的線程對象
        Thread t = Thread.currentThread();
		
		//獲取該線程的屬性:ThreadLocalMap
        ThreadLocalMap map = getMap(t);

		//如果map存在,則以當前ThreadLocal對象爲key,從Entry中設置key-value
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

	//獲取該線程的屬性:ThreadLocalMap
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

getMap這個方法,我們可以得出結論,threadLocals是線程的一個成員變量。
set方法,我們可以知道,threadLocals是一個ThreadLocalMap類型的變量,並且以當前的ThreadLocal對象爲key,線程要存放的局部變量爲value存放於map中。

由此可見,線程的成員變量threadLocals就是用來存放線程的局部變量的容器,是一個map結構。爲線程存放局部變量時,是以當前的ThreadLocal對象爲key,要存放的局部變量爲value存放數據的。另外,線程的局部變量是有線程對象管理的,而不是交給ThreadLocal管理的,因爲threadLocals是線程對象的屬性。


現在,我們知道了ThreadLocalMap是用來存放線程局部變量的容器,那麼我們接下來來了解ThreadLocalMap:

	//ThreadLocalMap類,是ThreadLocal的內部類
	static class ThreadLocalMap {
	
		// Entry類,是ThreadLocalMap的內部類
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

		private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
				...
            tab[i] = new Entry(key, value);
            	...
        }
     }
   }

通過以上代碼可知,ThreadLocalMapThreadLocal的內部類,Entry類是ThreadLocalMap的內部類。ThreadLocalMap的map結構實際上是通過Entry類型的數組實現的。

2.2 小結

  1. 每個Thread維護着一個存放線程局部變量的容器:ThreadLocalMap,其是一個map結構。
  2. ThreadLocalMapThreadLocal的內部類,其map結構是用Entry數組實現的,也就是數據最終存放在Entry對象中。
  3. 調用ThreadLocal的set()給線程設置局部變量時,實際上就是往ThreadLocalMap設置值,keyThreadLocal對象,值是傳遞進來的要設置局部變量。
  4. 調用ThreadLocalget()方法時,實際上就是往ThreadLocalMap獲取值,key是ThreadLocal對象。
  5. ThreadLocal本身並不存儲值,它只是作爲一個key來讓線程從ThreadLocalMap獲取value。

其關係,可以用如下一張圖表示:
在這裏插入圖片描述

3. 舉一個例子幫助理解

比如,我們在實現銀行轉賬的時候,A給B轉500元,具體步驟如下:

1. 從數據庫中讀取A的錢
2. 從數據庫中讀取B的錢
3. A的錢 - 500
4. B的錢 + 500
5. 將A現在的錢寫入數據庫
6. 將B現在的錢寫入數據庫

這6步中操作數據庫時,應該用的是同一個Connection連接對象,因爲這樣能夠保證這1,2,5,6操作數據庫時如果有一個沒有操作成功則整個6步都無效,從而保證了不會一方加錢,另一方不減錢的情況。

那麼如何實現這6步中操作數據庫時,應該用的是同一個Connection連接對象?
其實,我們只需要爲每個線程對象的局部變量中存放同一個Connection連接對象對象就可以實現。具體代碼如下:

public class DBUtil {
    //數據庫連接池
    private static BasicDataSource source;

    //爲不同的線程管理連接
    private static ThreadLocal<Connection> local;


    static {
        try {
            //加載配置文件
            Properties properties = new Properties();

            //獲取讀取流
            InputStream stream = DBUtil.class.getClassLoader().getResourceAsStream("連接池/config.properties");

            //從配置文件中讀取數據
            properties.load(stream);

            //關閉流
            stream.close();

            //初始化連接池
            source = new BasicDataSource();

            //設置驅動
            source.setDriverClassName(properties.getProperty("driver"));

            //設置url
            source.setUrl(properties.getProperty("url"));

            //設置用戶名
            source.setUsername(properties.getProperty("user"));

            //設置密碼
            source.setPassword(properties.getProperty("pwd"));

            //初始化線程本地
            local = new ThreadLocal<>();


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() throws SQLException {
        
        if(local.get()!=null){
            return local.get();
        }else{
        
            //獲取Connection對象
            Connection connection = source.getConnection();
    
            //把Connection放進ThreadLocal裏面
            local.set(connection);
    
            //返回Connection對象
            return connection;
        }

    }

    //關閉數據庫連接
    public static void closeConnection() {
        //從線程中拿到Connection對象
        Connection connection = local.get();

        try {
            if (connection != null) {
                //恢復連接爲自動提交
                connection.setAutoCommit(true);

                //這裏不是真的把連接關了,只是將該連接歸還給連接池
                connection.close();

                //既然連接已經歸還給連接池了,ThreadLocal保存的Connction對象也已經沒用了
                local.remove();

            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }


}

4. 內存泄露的問題

首先,簡單介紹一下一些相關術語:

  1. 內存泄漏:指程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。

  2. 強引用:指如果一個對象=null, 但是該對象在被其他對象使用,則該對象不會被垃圾回收機制回收。

  3. 弱引用:指如果一個對象=null, 但是該對象在被其他對象使用,則該對象會被垃圾回收機制回收。

看如下一段源碼:

		static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

可以看見,實際存儲類Entry對ThreadLocal對象是弱引用關係,也就是ThreadLocalMap對於key是弱引用關係。

爲什麼要設置成若引用關係?
你可以這樣想,如果ThreadLocal = null時,ThreadLocalMap中存放以ThreadLocal爲key的鍵值對是否應該刪除,ThreadLocal 是否應該刪除???
答案顯然是都應該刪除。如果是強引用,則都不能刪除,只有在該線程被回收時才都被刪除。那麼至少我們應該把能刪除的刪除了,弱引用關係能夠將ThreadLocal刪除,ThreadLocalMap中存放以ThreadLocal爲key的鍵值對通過其他手段刪除。

弱引用關係會造成,ThreadLocal = null時,ThreadLocal被回收,但是ThreadLocalMap中存放以ThreadLocal爲key的鍵值對沒有被回收,且無法被訪問,這樣就造成了內存泄漏,事實上早期是這樣的,現在這個問題被解決了。

解決的方法就是:在ThreadLocalMap中的set/getEntry方法中,會對key爲null(也即是ThreadLocal爲null)進行判斷,如果爲null的話,那麼是會對value置爲null的。當然,我們也可以手動的通過調用ThreadLocal的remove方法進行釋放!

參考文件:
https://blog.csdn.net/qq_42862882/article/details/89820017
https://www.jianshu.com/p/ee8c9dccc953

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