1. 程式人生 > >ThreadLocal 簡介 案例 源碼分析 MD

ThreadLocal 簡介 案例 源碼分析 MD

調用 通過 增量 rup 不可 最後一行 打印 getent adding

Markdown版本筆記 我的GitHub首頁 我的博客 我的微信 我的郵箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 [email protected]

ThreadLocal 簡介 案例 源碼分析 MD


目錄

目錄
先來看基本用法
對ThreadLocal的理解
ThreadLocal 詳解
正確理解 ThreadLocal
ThreadLocal 源碼分析
構造方法
set 方法
ThreadLocalMap
get 方法
總結
一個類型轉換的坑

先來看基本用法

class Person {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>(); //創建一個用於保存 Long 類型數據的 ThreadLocal
    ThreadLocal<String> stringLocal = new ThreadLocal<String>() {//創建一個用於保存 String 類型數據的 ThreadLocal
        @Override
        protected String initialValue() {
            return Thread.currentThread().getName(); //初始化數據第一種方式:重寫 initialValue 方法(推薦方式)
        }
    };
    //ThreadLocal.withInitial(()-> "返回初始值"); //初始化數據第三種方式,在1.8中添加的API

    public void setLongValue() {
        longLocal.set(Thread.currentThread().getId()); //初始化數據第二種方式:調用 set 方法
    }

    public long getLong() {
        return longLocal.get();
    }

    public String getString() {
        return stringLocal.get();
    }
}
public class Test {

    public static void main(String[] args) throws InterruptedException {
        final Person person = new Person();

        person.setLongValue(); //設置main線程中的對象的值
        System.out.println("A:" + person.getLong()); //1
        System.out.println("A:" + person.getString()); //main

        Thread thread = new Thread() {
            public void run() {
                person.setLongValue(); //設置子線程中的對象的值
                System.out.println("B:" + person.getLong()); //10
                System.out.println("B:" + person.getString()); //Thread-0
            };
        };
        thread.start();
        thread.join(); //效果等同於同步

        System.out.println("C:" + person.getLong()); //1
        System.out.println("C:" + person.getString()); //main
    }
}

打印結果:

A:1
A:main
B:10
B:Thread-0
C:1
C:main

對ThreadLocal的理解

參考

常用的幾個 API

ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
protected T initialValue() //一般是用來在使用時進行重寫的,它是一個延遲加載方法

public void set(T value)
public T get()
public void remove()

ThreadLocal提供了線程獨有的局部變量,可以在整個線程存活的過程中隨時取用

,極大地方便了一些邏輯的實現。常見的ThreadLocal用法有:

  • 存儲單個線程上下文信息
  • 使變量線程安全:變量既然成為了每個線程內部的局部變量,自然就不會存在並發問題了
  • 減少參數傳遞

原理
ThreadLocal裏類型的變量,其實是放入了當前Thread裏。每個Thread都有一個threadLocals,它是一個map,這個map的entry是ThreadLocal.ThreadLocalMap.Entry,具體的key和value類型分別是ThreadLocal和Object。

註:實際是ThreadLocal的弱引用 WeakReference<ThreadLocal<?>>,但可以先簡單理解為ThreadLocal。

對於一個普通的map,取其中某個key對應的值分兩步:

  • 找到這個map;
  • 在map中,給出key,得到value。

想取出我們存放在當前線程裏的map裏的值同樣需要這兩步,但是,我們不需要告訴jvm map在哪兒,因為jvm知道當前線程,也知道其局部變量map。所以最終的get操作只需要知道key(即ThreadLocal)就行了:longLocal.get()

為什麽key使用弱引用
不妨反過來想想,如果使用強引用,當ThreadLocal對象(假設為ThreadLocal@123456)的引用(即longLocal,是一個強引用,指向ThreadLocal@123456)被回收了,ThreadLocalMap本身依然還持有ThreadLocal@123456的強引用,如果沒有手動刪除這個key,則ThreadLocal@123456不會被回收,所以只要當前線程不消亡,ThreadLocalMap引用的那些對象就不會被回收,可以認為這導致Entry內存泄漏。

那使用弱引用的好處呢?

如果使用弱引用,那指向ThreadLocal@123456對象的引用就兩個:longLocal強引用,和ThreadLocalMap中Entry的弱引用。一旦longLocal被回收,則指向ThreadLocal@123456的就只有弱引用了,在下次gc的時候,這個ThreadLocal@123456就會被回收。

那麽問題來了,ThreadLocal@123456對象只是作為ThreadLocalMap的一個key而存在的,現在它被回收了,但是它對應的value並沒有被回收,內存泄露依然存在!而且key被刪了之後,變成了null,value更是無法被訪問到了!針對這一問題,ThreadLocalMap類的設計本身已經有了這一問題的解決方案,那就是在每次get()/set()/remove()ThreadLocalMap中的值的時候,會自動清理key為null的value。如此一來,value也能被回收了。

為什麽不對value使用弱引用
答案顯而易見,假設往ThreadLocalMap裏存了一個value,gc過後value便消失了,那就無法使用ThreadLocalMap來達到存儲全線程變量的效果了。

內存泄漏問題
弱引用一定程度上回收了無用對象,但前提是開發者手動清理掉ThreadLocal對象的強引用(如longLocal)。只要線程一直不死,ThreadLocalMap的key-value一直在漲。
解決方法是,當某個ThreadLocal變量不再使用時,調用 remove() 方法刪除該key。

使用線程池的問題
使用線程池可以達到線程復用的效果,但是歸還線程之前記得清除ThreadLocalMap,要不然再取出該線程的時候,ThreadLocal變量還會存在。這就不僅僅是內存泄露的問題了,整個業務邏輯都可能會出錯。

所以ThreadLocal最好還是不要和線程池一起使用。

ThreadLocal 詳解

參考

正確理解 ThreadLocal

首先,ThreadLocal不是用來解決共享對象的多線程訪問問題的,一般情況下,通過ThreadLocal.set()到線程中的對象是該線程自己使用的對象,其他線程是不需要訪問的,也訪問不到的,各個線程中訪問的是不同的對象。

另外,說ThreadLocal使得各線程能夠保持各自獨立的一個對象,並不是通過ThreadLocal.set()來實現的,而是通過每個線程中的new對象的操作來創建的對象,每個線程創建一個,不是什麽對象的拷貝或副本。通過ThreadLocal.set()將這個新創建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map,執行ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作為map的key來使用的

如果ThreadLocal.set()進去的東西本來就是多個線程共享的同一個對象,那麽多個線程的ThreadLocal.get()取得的還是這個共享對象本身,還是有並發訪問問題。

下面來看一個hibernate中典型的ThreadLocal的應用:

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(); //創建一個session
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

可以看到在getSession()方法中,首先判斷當前線程中有沒有放進去session,如果還沒有,那麽通過sessionFactory().openSession()來創建一個session,再將session set到線程中,實際是放到當前線程的ThreadLocalMap這個map中,這時,對於這個session的唯一引用就是當前線程中的那個ThreadLocalMap,而threadSession作為這個值的key,要取得這個session可以通過threadSession.get()來得到,裏面執行的操作實際是先取得當前線程中的ThreadLocalMap,然後將threadSession作為key將對應的值取出。

這個session相當於線程的私有變量,而不是public的。顯然,其他線程中是取不到這個session的,他們也只能取到自己的ThreadLocalMap中的東西。要是session是多個線程共享使用的,那還不亂套了。

試想如果不用ThreadLocal怎麽來實現呢?可能就要在action中創建session,然後把session一個個傳到service和dao中,這可夠麻煩的。或者可以自己定義一個靜態的map,將當前thread作為key,創建的session作為值,put到map中,應該也行,這也是一般人的想法。但事實上,ThreadLocal的實現剛好相反,它是在每個線程中有一個map,而將ThreadLocal實例作為key,這樣每個map中的項數很少,而且當線程銷毀時相應的東西也一起銷毀了

總之,ThreadLocal不是用來解決對象共享訪問問題的,而主要是提供了保持對象的方法和避免參數傳遞的方便的對象訪問方式。

歸納了兩點:

  • 每個線程中都有一個自己的ThreadLocalMap類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。
  • 將一個共用的ThreadLocal靜態實例作為key,將不同對象的引用保存到不同線程的ThreadLocalMap中,然後在線程執行的各處通過這個靜態ThreadLocal實例的get()方法取得自己線程保存的那個對象,避免了將這個對象作為參數傳遞的麻煩。

當然如果要把本來線程共享的對象通過ThreadLocal.set()放到線程中也可以,可以實現避免參數傳遞的訪問方式,但是要註意get()到的是那同一個共享對象,並發訪問問題要靠其他手段來解決。但一般來說線程共享的對象通過設置為某類的靜態變量就可以實現方便的訪問了,似乎沒必要放到線程中。

ThreadLocal的應用場合,我覺得最適合的是按線程多實例(每個線程對應一個實例)的對象的訪問,並且這個對象很多地方都要用到。

ThreadLocal 源碼分析

API

ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
protected T initialValue() //一般是用來在使用時進行重寫的,它是一個延遲加載方法

public void set(T value)
public T get()
public void remove()

變量

private final int threadLocalHashCode = nextHashCode();  //唯一的實例變量,而且還是不可變的
private static int nextHashCode = 0;  //靜態變量,表示即將分配的下一個ThreadLocal實例的threadLocalHashCode的值
private static final int HASH_INCREMENT = 0x61c88647; //常量,表示了連續分配的兩個ThreadLocal實例的threadLocalHashCode值的增量

構造方法

可以來看一下創建一個 ThreadLocal 實例即 new ThreadLocal() 時做了哪些操作

//Creates a thread local variable. @see #withInitial(java.util.function.Supplier)
public ThreadLocal() {
}

從上面看到構造函數 ThreadLocal() 裏什麽操作都沒有,唯一的操作是這句:

private final int threadLocalHashCode = nextHashCode();  

而 nextHashCode() 就是將 ThreadLocal 類的下一個 hashCode 值即 nextHashCode 的值賦給實例的 threadLocalHashCode,然後 nextHashCode 的值增加 HASH_INCREMENT 這個值。

因此,ThreadLocal實例的變量只有這個threadLocalHashCode,而且是final的,用來區分不同的ThreadLocal實例,ThreadLocal類主要是作為工具類來使用,那麽ThreadLocal.set()進去的對象是放在哪兒的呢?

set 方法

看一下set()方法:

//Sets the current thread‘s copy of this thread-local variable to the specified value.  
//Most subclasses will have no need to override this method, relying solely on the initialValue method to set the values of thread-locals.
//@param value :the value to be stored in the current thread‘s copy of this thread-local.
public void set(T value) {
    Thread t = Thread.currentThread(); //取得當前線程
    ThreadLocalMap map = t.threadLocals; //返回當前線程t中的一個成員變量 threadLocals
    if (map != null) map.set(this, value); //將該 ThreadLocal 實例(而不是當前線程)作為key,要保持的對象作為值
    else t.threadLocals = new ThreadLocalMap(this, value);
}

也就是將該 ThreadLocal 實例作為key,要保持的對象作為值,設置到當前線程的 ThreadLocalMap 中

ThreadLocalMap

這個 ThreadLocalMap 類是 ThreadLocal 中定義的內部類,但是它的實例卻用在Thread類中:

public class Thread implements Runnable {  
    //ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.
    ThreadLocal.ThreadLocalMap threadLocals = null;
}  

我們繼續取看 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 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<?>> {
        Object value;//The value associated with this ThreadLocal.

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

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

get 方法

看一下get()方法:

//Returns the value in the current thread‘s copy of this thread-local variable.
//If the variable has no value for the current thread, it is first initialized to the value returned by an invocation of the initialValue method.
//@return the current thread‘s value of this thread-local
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.threadLocals;
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); //註意這裏獲取鍵值對傳進去的是 ThreadLocal 實例,而不是當前線程 t
        if (e != null) return (T)e.value;
    }
    return setInitialValue();
}

再看 setInitialValue() 方法的具體實現:

//Variant of set() to establish initialValue. Used instead of set() in case user has overridden the set() method.
//@return the initial value
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.threadLocals; 
    if (map != null) map.set(this, value);
    else t.threadLocals = new ThreadLocalMap(this, value);
    return value;
}

可以看到,除了添加第一行和最後一行外,其他邏輯和 set 方法完全一樣。

第一行是調用 初始化方法 initialValue() 獲取初始值,中間就是保持的這個值,最後一行就是返回這個值。

protected T initialValue() {
    return null;
}

總結

  • 通過 ThreadLocal 創建的對象是存儲在每個線程自己的 threadLocals 集合中的
  • 集合 threadLocals 的類型為 ThreadLocalMap,存儲的實體為WeakReference<ThreadLocal<?>>,鍵為 ThreadLocal 對象
  • set 方法就是將該 ThreadLocal 實例作為 key,將要保持的對象作為值,設置到當前線程的 threadLocals 中
  • 在進行 get 之前,必須先 set,或者重寫 initialValue() 方法,否則返回的是 null

一個類型轉換的坑

如下代碼的執行結果是什麽:

public class Test {

    public static void main(String[] args) {
        System.out.println(new Person().getLong());
        System.out.println(new Person().getLong2());
    }
}

class Person {
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();

    public Long getLong() {
        return longLocal.get();
    }

    public long getLong2() {
        return longLocal.get();
    }
}

空指針異常

null
Exception in thread "main" java.lang.NullPointerException
    at Person.getLong2(Test.java:17)
    at Test.main(Test.java:5)

第二種寫法(返回基本類型 long 而非包裝類型 Long)是不是感覺很坑?

2019-1-21

ThreadLocal 簡介 案例 源碼分析 MD