天天看點

并發程式設計之深入了解threadlocal

前言:

相信有一些開發經驗的童鞋應該都聽過threadlocal,但是可能有一些隻是知道threadlocal的使用,并沒有真正了解threadlocal的工作的原理,以及在使用threadlocal中可能會遇到的問題,今天會從源碼的角度跟大家一起學習threadlocal使用的場景、常見的源碼中如何使用它,以及使用threadlocal應該注意什麼—記憶體洩露。

  • 1、Threadlocal的使用
  • 2、Threadlocal的源碼解析
  • 3、ThreadLocal記憶體洩漏的原因

T hreadLocal的使用

Threadlocal和synchronized都是用于解決多線程并發通路。然而ThreadLocal與synchronized有本質的差別。Synchronized是利用鎖的機制,使變量或者代碼塊在某一時刻僅能被一個線程通路。而ThreadLocal使用了線程隔離的方式為每一個線程都提供了變量的副本,使得每一個線程在某一時刻通路到的并非同一個對象。先來看下一個簡單的例子熱身一下,現在有建立三個線程,每個線程對threadlocal中存的值自增,看看列印結果

package threadLocal;

public class CounterTest {

    private final  ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    private int getCounter(){
        threadLocal.set(threadLocal.get()+1);
        return threadLocal.get();
    }

    public static void main(String[] args) {
        CounterTest counterTest = new CounterTest();
        Thread t1 = new Thread(new MyTask(counterTest));
        Thread t2 = new Thread(new MyTask(counterTest));
        Thread t3 = new Thread(new MyTask(counterTest));
        t1.start();
        t2.start();
        t3.start();
    }

    static class MyTask implements  Runnable{

        private final CounterTest counterTest;

        public MyTask(CounterTest counterTest) {
            this.counterTest = counterTest;
        }

        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                // ④每個線程打出3個序列值
                System.out.println("thread[" + Thread.currentThread().getName() + "] --> counterTest["
                        + counterTest.getCounter() + "]");
            }
        }
    }

}           

複制

執行結果:

并發程式設計之深入了解threadlocal

很和諧是不是,這就是用線程隔離的方法,使得每個線程都是使用了自己的線程内部的變量。那麼我們在哪些地方使用過threadlocal呢?其實我們使用的spring事務就是使用了Threadlocal類,spring會從資料庫連接配接池中擷取一個connection,該connection就是放在threadlocal中,也就是和線程綁定了,事務的送出或者復原,隻要從threadlocal中拿到connection進行操作。為何Spring的事務要借助ThreadLocal類?

以JDBC為例,正常的事務代碼可能如下:

dbc = new DataBaseConnection();//第1行

Connection con = dbc.getConnection();//第2行

con.setAutoCommit(false);// //第3行

con.executeUpdate(…);//第4行

con.executeUpdate(…);//第5行

con.executeUpdate(…);//第6行

con.commit();第7行

上述代碼,可以分成三個部分:

事務準備階段:第1~3行

業務處理階段:第4~6行

事務送出階段:第7行

可以很明顯的看到,不管我們開啟事務還是執行具體的sql都需要一個具體的資料庫連接配接。

現在我們開發應用一般都采用三層結構,如果我們控制事務的代碼都放在DAO(DataAccessObject)對象中,在DAO對象的每個方法當中去打開事務和關閉事務,當Service對象在調用DAO時,如果隻調用一個DAO,那我們這樣實作則效果不錯,但往往我們的Service會調用一系列的DAO對資料庫進行多次操作,那麼,這個時候我們就無法控制事務的邊界了,因為實際應用當中,我們的Service調用的DAO的個數是不确定的,可根據需求而變化,而且還可能出現Service調用Service的情況。

如果不使用ThreadLocal,代碼大概就會是這個樣子:

并發程式設計之深入了解threadlocal
并發程式設計之深入了解threadlocal

但是需要注意一個問題,如何讓三個DAO使用同一個資料源連接配接呢?我們就必須為每個DAO傳遞同一個資料庫連接配接,要麼就是在DAO執行個體化的時候作為構造方法的參數傳遞,要麼在每個DAO的執行個體方法中作為方法的參數傳遞。這兩種方式無疑對我們的Spring架構或者開發人員來說都不合适。為了讓這個資料庫連接配接可以跨階段傳遞,又不顯示的進行參數傳遞,就必須使用别的辦法。

Web容器中,每個完整的請求周期會由一個線程來處理。是以,如果我們能将一些參數綁定到線程的話,就可以實作在軟體架構中跨層次的參數共享(是隐式的共享)。而JAVA中恰好提供了綁定的方法–使用ThreadLocal。

結合使用Spring裡的IOC和AOP,就可以很好的解決這一點。

隻要将一個資料庫連接配接放入ThreadLocal中,目前線程執行時隻要有使用資料庫連接配接的地方就從ThreadLocal獲得就行了。

ThreadLocal源碼解析

其實threadlocal工作原理非常的簡單,我們先簡單的描述一下,然後再從一起閱讀分析源碼。

  1. threadlocal本身并不存儲内容
  2. 存儲内容的thread對象中的ThreadLocalMap(可以當作Map了解)
  3. threadLocal是充當Thread對象中ThreadLocalMap中的key值

簡單的用下面一幅圖表示一下,是以每個線程取到的值,其實是他自己線程内部的值

并發程式設計之深入了解threadlocal

如果結合上面的文字和圖,你能看懂的話,那麼恭喜你,threadLocal的工作原理,你基本上已經了解了。下面我們就來詳細的看看threadLocal的源碼,這裡主要介紹threadLocal中的4個方法:

public void set(T value)

設定目前線程的線程局部變量的值

public T get()

傳回目前線程所對應的線程局部變量

public void remove()

将目前線程局部變量的值删除,目的是為了減少記憶體占用,該方法是JDK5.0新增的方法,需要注意的是,當線程結束後(那如果線程不結束呢?),對應該線程的局部變量将會自動被垃圾回收,是以顯示調用該方法清除線程局部變量并不是必須的操作,但它可以加快記憶體的回收速度。

pretected T initialValue()

傳回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆寫而設計的,這個方法是一個延遲調用方法,線上程第一次調用get()或者set(T t)時才運作,并且僅執行一次,ThreadLocalz中預設實作直接傳回一個null

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

複制

get方法非常的簡單,就是擷取目前線程的ThreadLocalMap,然後将this(表示該ThreadLocal)當作key值,擷取相應的value,如果擷取不到,那麼就傳回初始設定的值。

2、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);
}           

複制

這個set方法也是簡單的不要不要的啦,首先也是擷取目前的線程啦,然後擷取目前線程的ThreadLocalMap,如果map不為null那麼就往該線程的ThreadLocalMap中set一條資料,key為this(該threadLocal),value為傳進去的值,這裡解釋一下,ThreadLocalMap可以簡單的了解成Map(雖然和Map不一樣),它裡面使用一個table[]存儲鍵值對(Entry)。

3、remove方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}           

複制

remove也簡單,同樣也是擷取目前線程的ThreadLocalMap,然後删除該key(ThreadLocal)對應的鍵值對

4、initialValue()方法

4、protected T initialValue() {
    return null;
}           

複制

initialValue一般用于我們自己初始化,比如,我們需要存儲Integer類型的值,那麼可以重寫initialValue方法,然後傳回0對應的Integer值。

看到上面的四個方法的源碼是不是驚呆啦,不過如此嘛,是不是so easy!但是我們今天學習可不僅僅到這裡就結束了哦,我們需要繼續學習使用ThreadLocal可能會存在什麼樣的問題—記憶體洩露,這是我們在面試過程中經常被問到的内容,同時也讓我們學的代碼更加安全、健壯、穩定

ThreadLocal記憶體洩漏原因及解決方法

在分析ThreadLocal記憶體洩漏原因之前,希望大家最好先了解強引用、軟引用、弱引用、虛引用。

強引用:就是指在程式代碼之中普遍存在的,類似“Object obj=new Object()”這類的引用,隻要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象執行個體。

軟引用:是用來描述一些還有用但并非必需的對象。對于軟引用關聯着的對象,在系統将要發生記憶體溢出異常之前,将會把這些對象執行個體列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出異常。在JDK 1.2之後,提供了SoftReference類來實作軟引用。

弱引用:也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象執行個體隻能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象執行個體。在JDK 1.2之後,提供了WeakReference類來實作弱引用。

虛引用:也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象執行個體是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象執行個體。為一個對象設定虛引用關聯的唯一目的就是能在這個對象執行個體被收集器回收時收到一個系統通知。在JDK 1.2之後,提供了PhantomReference類來實作虛引用。

了解上述内容之後,我們來看下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的key是用弱引用,那麼到下一次垃圾回收就會被垃圾收集器回收掉,然而我們就永遠擷取不到該key對應的value,然而這個value又被thread中的threadlocal.threadLocalMap給強引用(如果線程沒有結束),那麼value将不會被回收,又通路不了,那麼這個時候就産生了記憶體洩漏啦,用下面的圖來簡單的描述一下。

并發程式設計之深入了解threadlocal

然而,在前面的描述中已經提到過,當線程結束時,對應value也會被删除,這個時候不會産生記憶體洩漏,那什麼時候我們不會讓線程結束呢?—就是我們在使用線程池的時候,我們是會保留一部分線程放線上程池中,這個時候使用threadlocal就要注意啦,如果操作不注意就會産生記憶體洩漏啦。這個時候我們使用完之後可以手動調用threadlocal.remove()就可以将對應value删除掉,防止記憶體洩漏。

從上面的分析中貌似是使用了弱引用導緻的記憶體洩漏,那為什麼jdk不使用強引用呢?

使用強引用:threadlocal使用完被置null時,我們結合上面的圖來分析一下,theadlocal=null,但是currentThread還是持有key-value的強引用,那麼key和value都不會被回收,但是又通路不了,同樣是産生了記憶體洩漏。

使用弱引用:當threadlocal對應執行個體被置null時,那麼currentthread中key(threadlocal)會被垃圾回收器回收掉,其對應的value在下一次threadlocal調用你set、get、remove方法時都有機會被回收掉。

比較上述兩種情況,我們可以發現:由于ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動删除對應key,都會導緻記憶體洩漏,但是使用弱引用可以多一層保障。

是以,ThreadLocal記憶體洩漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動删除對應key就會導緻記憶體洩漏,而不是因為弱引用。

好了,threadlocal就分享到這裡,如有錯誤,歡迎指正!!!

釋出者:全棧程式員棧長,轉載請注明出處:https://javaforall.cn/111217.html原文連結:https://javaforall.cn