1. 程式人生 > >Java多執行緒:ThreadLocal解析

Java多執行緒:ThreadLocal解析

一、ThreadLocal是什麼

ThreadLocal是一個本地執行緒副本變數工具類。主要用於將私有執行緒和該執行緒存放的副本物件做一個對映,各個執行緒之間的變數互不干擾,在高併發場景下,可以實現無狀態的呼叫,特別適用於各個執行緒依賴不通的變數值完成操作的場景。

二、從資料結構入手

下圖為ThreadLocal的內部結構圖

ThreadLocal結構內部

從上面的結構圖,我們已經窺見ThreadLocal的核心機制:

  • 每個Thread執行緒內部都有一個Map。
  • Map裡面儲存執行緒本地物件(key)和執行緒的變數副本(value)
  • 但是,Thread內部的Map是由ThreadLocal維護的,由ThreadLocal負責向map獲取和設定執行緒的變數值。

所以對於不同的執行緒,每次獲取副本值時,別的執行緒並不能獲取到當前執行緒的副本值,形成了副本的隔離,互不干擾。

Thread執行緒內部的Map在類中描述如下:

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

三、深入解析ThreadLocal

ThreadLocal類提供如下幾個核心方法:

public T get()
public void set(T value)
public void remove()
  • get()方法用於獲取當前執行緒的副本變數值。
  • set()方法用於儲存當前執行緒的副本變數值。
  • initialValue()為當前執行緒初始副本變數值。
  • remove()方法移除當前前程的副本變數值。

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 {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
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();
}

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

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

protected T initialValue() {
    return null;
}

步驟:
1.獲取當前執行緒的ThreadLocalMap物件threadLocals
2.從map中獲取執行緒儲存的K-V Entry節點。
3.從Entry節點獲取儲存的Value副本值返回。
4.map為空的話返回初始值null,即執行緒變數副本為null,在使用時需要注意判斷NullPointerException。

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 {@link #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 = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

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

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

步驟:
1.獲取當前執行緒的成員變數map
2.map非空,則重新將ThreadLocal和新的value副本放入到map中。
3.map空,則對執行緒的成員變數ThreadLocalMap進行初始化建立,並將ThreadLocal和value副本放入map中。

remove()方法

/**
 * Removes the current thread's value for this thread-local
 * variable.  If this thread-local variable is subsequently
 * {@linkplain #get read} by the current thread, its value will be
 * reinitialized by invoking its {@link #initialValue} method,
 * unless its value is {@linkplain #set set} by the current thread
 * in the interim.  This may result in multiple invocations of the
 * <tt>initialValue</tt> method in the current thread.
 *
 * @since 1.5
 */
public void remove() {
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null)
     m.remove(this);
}

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

remove方法比較簡單,不做贅述。

四、ThreadLocalMap

ThreadLocalMap是ThreadLocal的內部類,沒有實現Map介面,用獨立的方式實現了Map的功能,其內部的Entry也獨立實現。

ThreadLocalMap類圖

在ThreadLocalMap中,也是用Entry來儲存K-V結構資料的。但是Entry中key只能是ThreadLocal物件,這點被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繼承自WeakReference(弱引用,生命週期只能存活到下次GC前),但只有Key是弱引用型別的,Value並非弱引用。

ThreadLocalMap的成員變數:

static class ThreadLocalMap {
    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0
}

Hash衝突怎麼解決

和HashMap的最大的不同在於,ThreadLocalMap結構非常簡單,沒有next引用,也就是說ThreadLocalMap中解決Hash衝突的方式並非連結串列的方式,而是採用線性探測的方式,所謂線性探測,就是根據初始key的hashcode值確定元素在table陣列中的位置,如果發現這個位置上已經有其他key值的元素被佔用,則利用固定的演算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。

ThreadLocalMap解決Hash衝突的方式就是簡單的步長加1或減1,尋找下一個相鄰的位置。

/**
 * Increment i modulo len.
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * Decrement i modulo len.
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

顯然ThreadLocalMap採用線性探測的方式解決Hash衝突的效率很低,如果有大量不同的ThreadLocal物件放入map中時傳送衝突,或者發生二次衝突,則效率很低。

所以這裡引出的良好建議是:每個執行緒只存一個變數,這樣的話所有的執行緒存放到map中的Key都是相同的ThreadLocal,如果一個執行緒要儲存多個變數,就需要建立多個ThreadLocal,多個ThreadLocal放入Map中時會極大的增加Hash衝突的可能。

ThreadLocalMap的問題

由於ThreadLocalMap的key是弱引用,而Value是強引用。這就導致了一個問題,ThreadLocal在沒有外部物件強引用時,發生GC時弱引用Key會被回收,而Value不會回收,如果建立ThreadLocal的執行緒一直持續執行,那麼這個Entry物件中的value就有可能一直得不到回收,發生記憶體洩露。

如何避免洩漏
既然Key是弱引用,那麼我們要做的事,就是在呼叫ThreadLocal的get()、set()方法時完成後再呼叫remove方法,將Entry節點和Map的引用關係移除,這樣整個Entry物件在GC Roots分析後就變成不可達了,下次GC的時候就可以被回收。

如果使用ThreadLocal的set方法之後,沒有顯示的呼叫remove方法,就有可能發生記憶體洩露,所以養成良好的程式設計習慣十分重要,使用完ThreadLocal之後,記得呼叫remove方法。

ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
    threadLocal.set(new Session(1, "Misout的部落格"));
    // 其它業務邏輯
} finally {
    threadLocal.remove();
}

五、應用場景

還記得Hibernate的session獲取場景嗎?

private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();

//獲取Session
public static Session getCurrentSession(){
    Session session =  threadLocal.get();
    //判斷Session是否為空,如果為空,將建立一個session,並設定到本地執行緒變數中
    try {
        if(session ==null&&!session.isOpen()){
            if(sessionFactory==null){
                rbuildSessionFactory();// 建立Hibernate的SessionFactory
            }else{
                session = sessionFactory.openSession();
            }
        }
        threadLocal.set(session);
    } catch (Exception e) {
        // TODO: handle exception
    }

    return session;
}

為什麼?每個執行緒訪問資料庫都應當是一個獨立的Session會話,如果多個執行緒共享同一個Session會話,有可能其他執行緒關閉連線了,當前執行緒再執行提交時就會出現會話已關閉的異常,導致系統異常。此方式能避免執行緒爭搶Session,提高併發下的安全性。

使用ThreadLocal的典型場景正如上面的資料庫連線管理,執行緒會話管理等場景,只適用於獨立變數副本的情況,如果變數為全域性共享的,則不適用在高併發下使用。

六、總結

  • 每個ThreadLocal只能儲存一個變數副本,如果想要上線一個執行緒能夠儲存多個副本以上,就需要建立多個ThreadLocal。
  • ThreadLocal內部的ThreadLocalMap鍵為弱引用,會有記憶體洩漏的風險。
  • 適用於無狀態,副本變數獨立後不影響業務邏輯的高併發場景。如果如果業務邏輯強依賴於副本變數,則不適合用ThreadLocal解決,需要另尋解決方案。

相關推薦

Java執行ThreadLocal解析

一、ThreadLocal是什麼 ThreadLocal是一個本地執行緒副本變數工具類。主要用於將私有執行緒和該執行緒存放的副本物件做一個對映,各個執行緒之間的變數互不干擾,在高併發場景下,可以實現無狀態的呼叫,特別適用於各個執行緒依賴不通的變數值完成操作的場景。 二、從

java執行ExecutorService解析(五)

       前面幾篇部落格寫到的多執行緒實現基本都是顯式呼叫了Thread的start()方法,除了這種方法有沒有其他的實現方法呢,這裡我們來看下java.util.concurrent包下的Exe

Java執行神祕的執行變數 ThreadLocal 你瞭解嗎?

前言 在 Java多執行緒中,執行緒變數ThreadLocal非常重要,但對於很多開發者來說,這並不容易理解,甚至覺得有點神祕 今天,我將獻上一份 ThreadLocal的介紹 & 實戰攻略,希望你們會喜歡。 目錄 1. 簡

Java執行之深入解析ThreadLocal和ThreadLocalMap

ThreadLocal概述 ThreadLocal是執行緒變數,ThreadLocal中填充的變數屬於當前執行緒,該變數對其他執行緒而言是隔離的。ThreadLocal為變數在每個執行緒中都建立了一個副本,那麼每個執行緒可以訪問自己內部的副本變數。 它具有3個特性: 執行緒併發:在多執行緒併發場景下使用。

Java執行之——ThreadLocal

ThreadLocal是什麼:每一個ThreadLocal能夠放一個執行緒級別的變數,也就是說,每一個執行緒有獨自的變數,互不干擾。以此達到執行緒安全的目的,並且一定會安全。 實現原理: 要了解實現原理,我們先看set方法 public void set(T value) { T

Java執行程式設計詳細解析

Java多執行緒程式設計詳細解析   一、理解多執行緒多執行緒是這樣一種機制,它允許在程式中併發執行多個指令流,每個指令流都稱為一個執行緒,彼此間互相獨立。執行緒又稱為輕量級程序,它和程序一樣擁有獨立的執行控制,由作業系統負責排程,區別在於執行緒沒有獨立的儲存空間,而是和所屬程

java執行5.1 鎖-基礎

什麼是鎖 提到多執行緒,立馬就有人說加鎖,什麼是鎖,為什麼加鎖? 鎖:從字面意義,什麼東西加了鎖,那麼就只有有鑰匙的人才能使用,多執行緒中的鎖,也是這個意思。 為什麼加鎖:當單執行緒的時候,無論訪問什麼資源,都不需要考慮鎖的問題,但是當多個執行緒訪問同一個資源,就會發生很多千奇百怪的

java執行3 執行狀態

執行緒從建立到結束執行,經過多個狀態,每個狀態對應著不同的操作,具體如下: 1 建立(new),2 可以執行(runnable)、3 執行(running)、4 等待(waiting)、5 阻塞(blocked)、6 結束(dead) 建立執行緒 當通過new Thre

java執行2 建立執行

如何建立一個多執行緒: Java提供了java.lang.Thread類,這就是執行緒的定義類,包含了:執行緒的優先順序,執行緒id、執行緒狀態等執行緒的基本資訊。 通過Thread類的說明,可以知道建立執行緒的兩種方式:1 extends Thread類,2 implements

java執行1程序和執行

多執行緒樣例 我們用工人卸貨舉例:有一集裝箱的貨物等待卸車,共100個箱子,一個工人一次只能搬一個箱子。 如果只有一個工人,那麼該工人需要搬運100次,而且是不停歇的搬運。 如果有5個或者10個工人,那麼平均每個工人只需要搬運20或者10次就可以了。 甚至有1

Java執行 死鎖

                         

java執行執行的通訊、喚醒等待機制、生產消費者模式、Lock

執行緒的通訊:印表機列印–不斷的輸入輸出 package com.qianfeng.test; /* * 執行緒的通訊: * 分析: * 需要兩個執行緒--輸入執行緒和輸出執行緒 * 需要兩個任務--輸入任務和輸出任務 * 需要一份資料 * *

Java執行執行基礎

多執行緒基礎 多執行緒實現-Thread和Runnable 通常使用如下程式碼啟動一個新的執行緒: private void startNewThread1() { new Thread() { @Override pub

Java執行——FutureTask原始碼解析

一個很常見的多執行緒案例是,我們安排主執行緒作為分配任務和彙總的一方,然後將計算工作切分為多個子任務,安排多個執行緒去計算,最後所有的計算結果由主執行緒進行彙總。比如,歸併排序,字元頻率的統計等等。 我們知道Runnable是不返回計算結果的,如果想利用多執行緒的話,只能儲

初學Java執行執行返回資料的兩種方法

從執行緒中返回資料和向執行緒傳遞資料類似。也可以通過類成員以及回撥函式來返回資料。但類成員在返回資料和傳遞資料時有一些區別,下面讓我們來看看它們區別在哪。 一、通過類變數和方法返回資料 使用這種方法返回資料需要在呼叫start方法後才能通過類變數或方法得到資料。讓我們先來看

Java 執行池ThreadPoolExecutor解析及Executors類中提供的靜態方法來建立執行

上面的程式碼可能看起來不是那麼容易理解,下面我們一句一句解釋:   首先,判斷提交的任務command是否為null,若是null,則丟擲空指標異常;   接著是這句,這句要好好理解一下: if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(c

java執行3、Java執行的支援1

宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 1、Java在語言級提供了對多執行緒程式設計的支援。 2、實現多執行緒程式的兩種方式 3、後臺執行緒 4、yield方法 5、執行緒優先順序 1、Java在語言級提

java執行2、單執行程式與執行程式

宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 單執行緒程式 多執行緒程式 單執行緒程式 在程序的地址空間當中,只有一個執行緒,由這一個執行緒來完成我們程式的執行任務。 如:這就好像有一個患者要動手術,醫院由於人手緊

java執行1、程式、程序和執行

宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 程式 程序 執行緒 程式 是計算機指令的集合,它以檔案的形式儲存在磁碟上。   程序 是一個程式在其自身的地址空間中的一次執行活動。 如:當我

java執行volatile及Timer用法示例

最近寫程式時,需要在java中使用多執行緒共享程式變數。 特別地,我需要另一個執行緒中執行類A的方法function_1。但給定的執行時間只有n毫秒,也就是說我會在n毫秒之後終止function_1的執行。 但問題是,我在主執行緒隨後的程式碼中無法判斷funcion_1是壽