ThreadLocal全面講解

概述

ThreadLocal無論是在平常的項目中還是面試都會比較頻繁的出現,今天星期天,抽時間總結一下。首先ThreadLocal的出現是爲解決多線程程序的併發問題的,ThreadLocal提供了線程內存儲變量的能力,這些變量不同之處在於每一個線程讀取的變量是對應的互相獨立的。各個線程通過ThreadLocal的get和set方法就可以設置和得到當前線程對應的值。這樣說起來比較抽象,我們下面通過實際的案例來解釋說明。

目錄

一、ThreadLocal的基本使用

二、ThrealLocal實現原理

三、ThreadLocal的內存泄漏問題


一、ThreadLocal的基本使用

爲了說明ThreadLocal的作用,我們先看一下沒有ThreadLocal的情況

public class ThreadLocalBDemo{
    static Integer num ;
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() +":" + num);
                num = 1;
                System.out.println(Thread.currentThread().getName() + ":" + num);
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() +":" +  num);
                num = 2;
                System.out.println(Thread.currentThread().getName() + ":" + num);
            }
        });

        thread1.start();
        try {
            /**
             *加入這個join方法主要是爲了讓線程thread1和thread2順序執行
             * 我們在main線程中調用了thread1的join方法,那麼main線程就會被阻塞
             * 等到thread1線程執行完後,main線程纔會繼續執行,所以在main線程中的
             * thread2.start()纔會執行,這樣就保證了thread1和thread2順序執行
             */
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }
}

我們在ThreadLocalBDemo定義了一個全局變量 num,並且開啓兩個線程thread1和thread2,在兩個線程中分別對num進行賦值處理,看一下輸出結果:

Thread-0:null
Thread-0:1
Thread-1:1
Thread-1:2

通過輸出結果我們可以看出,我們在thread1中給num賦值爲1後,在thread2中的第一個輸出語句中也拿到num=1的結果。在一些業務需求中我們不希望這個變量線程共享,而是每個線程都有屬於它自己獨立的一份,比如在實際業務中我們每個線程代表一個用戶,這個num代表每個用戶自己特有的訂單數量等屬性,那麼像上面的程序就很明顯不符合業務需求和邏輯。這個時候我們引入了ThrealLocal,ThreadLocal會爲這個變量保存一個只屬於它自己線程本身的變量副本,從而避免上面兩個線程相互影響的情況。我們還是以上面的程序爲案例,用ThreadLocal對其進行改造:

package com.zhangxudong;

public class ThreadLocalDemo {
   static ThreadLocal<Integer> threadLocal =  new ThreadLocal<Integer>();
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(threadLocal.get());
                threadLocal.set(1);
                System.out.println(threadLocal.get());
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(threadLocal.get());
                threadLocal.set(2);
                System.out.println(threadLocal.get());
            }
        });

        thread1.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }
}

輸出結果:

null
1
null
2

通過上面的輸出結果我們可以看到雖然我在先執行的thread1中通過ThreadLocal的set方法賦值爲1,但在後執行的thread2中的第一行輸出語句中,通過get方法拿到的值確實null,這就實現了變量的隔離。那ThreadLocal是怎麼實現的這種各個線程變量隔離的呢?下面我們就進入ThreadLocal源碼內部瞭解一下。

二、ThrealLocal實現原理

首先我們看一下ThreadLocal在使用set方法賦值的時候,是怎麼實現爲每個線程保存一個變量副本的。我們先從set()方法入手:

set()方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

可以看到,我們在thread1線程中利用ThreadLocal的set方法設置值的時候,在set方法內部首先是通過Thread t = Thread.currentThread()拿到當前的線程,這個線程當然就是thread1本身。緊接着調用了getMap(t)方法,這個方法傳進去的參數就是當前線程thread1,那我們繼續看一下getMap(t)這個方法:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可以看到它返回值是一個ThreadLocalMap類型的threadLocals變量,這個threadLocals其實就是線程類Thread中的一個成員變量,當前這個時候也就是thread1實例對象中的threadLocals成員變量:

//Thread類中的成員變量
ThreadLocal.ThreadLocalMap threadLocals = null;

那麼這個ThreadLocalMap又是一個什麼東西呢?這個你就簡單的先理解爲它就是一個類似我們平常用的Map結構,裏面存放的是(key,value)鍵值對形式的數據。上面介紹的一切,總結起來其實就是一句話:在線程類中有一個成員變量threadLocals,這個成員變量的類型是ThreadLocalMap類型的,我們可以往裏面放一條條“鍵值對”形式的數據。所以我們每new一個線程,那麼在這個線程對象了裏就有一個只屬於這個線程對象的ThreadLocalMap類型的threadLocals變量。

繼續接着上面的set源碼看,通過getMap(t)方法在拿到這個屬於當前線程對象(thread1)的threadLocals變量後(map),判斷其是否爲空,如果爲空的話createMap()方法創建一個,如果不爲空的話,就往裏面存一個(key,value)形式的鍵值對,其中的這個key就是this,表示我們最開始創建的ThreadLocal對象,value就是我們最開始要設置的值。所以不管我們創建多少個線程實例,在每個線程實例中都有一個屬於它自己的threadLocals變量,通過set方法往裏面放值的時候,也是放到了只屬於這個線程的threadLocals中去(雖然各個線程中threadLocals的key是一樣的,都是我們最開始創建的ThreadLocal對象,但threadLocals本身是不一樣的,它屬於不同的線程,這裏可能有點繞,好好理解一下),所以我們在拿值得時候,也就會只從屬於當前線程得threadLocals中去拿,這就實現了變量的隔離。到此,ThreadLocal的set方法就講解完了。

get()方法

理解了set方法其實get方法就很好理解了,我們看一下get方法得源碼:

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

我們可以看到,get方法中首先也是拿到當前線程,然後也是通過getMap()方法拿到屬於它得threadLocals,如果這個threadLocals不爲空,則通過getEntry()方法得到存在裏面的鍵值對Entry類型的對象,這個鍵值對中的鍵就是ThreadLocal對象,值就是我們之前在set方法中設置的值,最後的e.value拿到的就是這個值。

三、ThreadLocal的內存泄漏問題

在上面講解THreadLocal的set()方法的時候我們知道,set方法會把ThreadLocal對象作爲鍵,具體的value作爲值,以鍵值對的形式存放在ThreadLocalMap類型的threadLocals變量中,而這個鍵值對整體是以一個Entry類型存在的。我們接着上面set源碼中的map.set(this, value)繼續點進去看:

private void set(ThreadLocal key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

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

上面的源碼我們只看第8行和第28行就可以了,其他的先忽略,首先在這個set方法中的最開始有這麼一行代碼Entry[] tab = table,它維護了一個Entry類型的數組tab;接着看第28行:tab[i] = new Entry(key, value),可以發現是根據我們傳過來的key和value值來new了一個Entry對象,並把它放到了Entry數組tab中。整體關係就是Thread中有一個ThreadLocalMap類型的成員變量threadLocals,而在ThreadLocalMap是ThreadLocal類的一個靜態內部類,Entry又是ThreadLocalMap內部的一個靜態內部類。  我們繼續往下走,看看Entry的具體細節:

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

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

Entry 是ThreadLocalMap的一個靜態內部類,並且它繼承了弱引用(關於弱引用可以參考我前篇的文章java中四種引用類型),在Entry的構造方法中有這麼一段:super(k),這說明它調用了其父類“弱應用WeakReference”的構造方法,並且將鍵值k傳了進去(這個key值就是指向ThreadLocal對象的引用)。那麼這個時候key就以一種弱引用的形式指向了ThreadLocal對象,我們通過一張圖來說明一下這個:

                      

首先我們在最開始通過ThreadLocal t1 = new ThreadLocal()這新建一個ThreadLocal對象時,這個t1是一個強引用並且執行ThreadLocal對象。之後通過一系列上面的操作,我們知道鍵值對Entry中的key會以弱引用的形式也指向Thread Local對象。

那爲什麼Entry要使用弱引用呢?因爲若使用的是強引用,即使t1=null,但key依然以強引用的形式指向ThreadLocal對象,因此ThreadLocal對象不會被釋放掉,所以會有內存泄漏的問題。但如果是弱引用,當t1=null後,因爲key是以弱引用的形式指向ThreadLocal的對象,那麼gc碰到弱引用它是不關心的,依然會把ThreadLocal對象回收掉。但是雖然這樣,ThreadLocal還是會存在內存泄漏問題,因爲當ThreadLocal對象被回收後,key值就變成了null,這就導致整個Entry中的value再也無法訪問到,因此依然存在內存泄漏問題。綜上,ThreadLocal是存在內存泄漏的 問題的

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