java併發編程之ThreadLocal

ThreadLocal是什麼?

  • ThreadLocal,看名字不少第一次見到的人會認爲是一個線程,將其歸結到Thread。其實,咱們能夠這樣想ThreadLocal是Thread修飾的Local,這個Local纔是主角。ThreadLocal並非一個Thread,而是Thread的局部變量。線程局部變量(ThreadLocal)的功用很是簡單,就是爲每個使用該變量的線程都提供一個變量值的副本,是Java中一種較爲特殊的線程綁定機制,是每個線程均可以獨立地改變本身的副本,而不會和其它線程的副本衝突。
  • 經過ThreadLocal存取的數據,老是與當前線程相關,也就是說,JVM 爲每一個運行的線程,綁定了私有的本地實例存取空間,從而爲多線程環境常出現的併發訪問問題提供了一種隔離機制。ThreadLocal是如何作到爲每個線程維護變量的副本的呢?其實實現的思路很簡單,在ThreadLocal類中有一個Map,用於存儲每個線程的變量的副本。歸納起來講,ThreadLocal爲每個線程都提供了一份變量,所以能夠同時訪問而互不影響。

API

1. ThreadLocal()

建立一個線程本地變量。程序員

2. T get()

返回此線程局部變量的當前線程副本中的值,若是這是線程第一次調用該方法,則建立並初始化此副本。數據庫

3. protected T initialValue()

  • 返回此線程局部變量的當前線程的初始值。最多在每次訪問線程來得到每一個線程局部變量時調用此方法一次,即線程第一次使用 get() 方法訪問變量的時候。若是線程先於 get 方法調用 set(T) 方法,則不會在線程中再調用 initialValue 方法。api

  • 若該實現只返回 null;若是程序員但願將線程局部變量初始化爲 null 之外的某個值,則必須爲 ThreadLocal 建立子類,並重寫此方法。一般,將使用匿名內部類。initialValue 的典型實現將調用一個適當的構造方法,並返回新構造的對象。安全

4. void remove()

移除此線程局部變量的值。這可能有助於減小線程局部變量的存儲需求。session

5. void set(T value)

將此線程局部變量的當前線程副本中的值設置爲指定值。多線程

ThreadLocal使用示例

public class Main {

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {

            new Thread(){
                @Override
                public void run(){
                    System.out.println("當前線程: " + Thread.currentThread().getName() + ", 已分配id: " + ThreadId.get());
                }
            }.start();
        }
    }

    static class ThreadId{

        private static final AtomicInteger id = new AtomicInteger(0);

        private static final ThreadLocal<Integer> local = new ThreadLocal<>(){
            @Override
            protected Integer initialValue(){
                return id.getAndIncrement();
            }
        };

        // 返回當前線程的惟一的序列,若是第一次get,會先調用initialValue,後面看源碼就瞭解了
        public static int get(){
            return local.get();
        }
    }
}

控制檯打印結果:
當前線程: Thread-2, 已分配id: 1
當前線程: Thread-0, 已分配id: 2
當前線程: Thread-3, 已分配id: 4
當前線程: Thread-1, 已分配id: 0
當前線程: Thread-4, 已分配id: 3

ThreadLocal源碼分析

1. public void set(T value)

public void set(T value) {
        Thread t = Thread.currentThread(); // 取當前線程
        ThreadLocalMap map = getMap(t); // 和當前線程關聯的Map對象

        if (map != null) {
            map.set(this, value); // this是當前ThreadLocal對象,將value映射到和當前線程相關的Map中
        } else {
            createMap(t, value); // 不存在則建立
        }
    }
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

能夠看到ThreadLocalMap是Thread對象中的一個屬性。每一個Thread對象都有一個ThreadLocal.ThreadLocalMap成員變量,ThreadLocal.ThreadLocalMap是一個ThreadLocal類的靜態內部類(以下所示),因此Thread類能夠進行引用.因此每一個線程都會有一個ThreadLocal.ThreadLocalMap對象的引用。併發

通俗的講,每個Thread對象都有一個ThreadLocal.ThreadLocalMap成員變量(做爲線程私有變量,從而也是線程安全的),而這些成員變量能夠代理給ThreadLocal進行管理。ide

2. public T 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(); // 若是Map中已經存在值,不論是set()方法設置,仍是已經初始化過,都再也不調用
    }

對於ThreadLocal須要注意的有兩點:工具

  1. ThreadLocal實例自己是不存儲值,它只是提供了一個在當前線程中找到副本值得key。
  2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小夥伴會弄錯他們的關係。

ThreadLocal的應用場景

  1. 最多見的ThreadLocal使用場景爲 用來解決 數據庫鏈接、Session管理等
/**
 * 數據庫鏈接管理類
 */
public class ConnectionManager {
 
    /** 線程內共享Connection,ThreadLocal一般是全局的,支持泛型 */
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
    
    public static Connection getCurrConnection() {
        // 獲取當前線程內共享的Connection
        Connection conn = threadLocal.get();
        try {
            // 判斷鏈接是否可用
            if(conn == null || conn.isClosed()) {
                // 建立新的Connection賦值給conn(略)
                // 保存Connection
                threadLocal.set(conn);
            }
        } catch (SQLException e) {
            // 異常處理
        }
        return conn;
    }
    
    /**
     * 關閉當前數據庫鏈接
     */
    public static void close() {
        // 獲取當前線程內共享的Connection
        Connection conn = threadLocal.get();
        try {
            // 判斷是否已經關閉
            if(conn != null && !conn.isClosed()) {
                // 關閉資源
                conn.close();
                // 移除Connection
                threadLocal.remove();
                conn = null;
            }
        } catch (SQLException e) {
            // 異常處理
        }
    }
}
  1. Hiberante的Session 工具類HibernateUtil
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory; //定義SessionFactory
static {
try {
// 經過默認配置文件hibernate.cfg.xml建立SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("初始化SessionFactory失敗!", ex);
throw new ExceptionInInitializerError(ex);
}
}
//建立線程局部變量session,用來保存Hibernate的Session
public static final ThreadLocal session = new ThreadLocal();
/**
* 獲取當前線程中的Session
* @return Session
* @throws HibernateException
*/
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// 若是Session尚未打開,則新開一個Session
if (s == null) {
s = sessionFactory.openSession();
session.set(s); //將新開的Session保存到線程局部變量中
}
return s;
}
public static void closeSession() throws HibernateException {
//獲取線程局部變量,並強制轉換爲Session類型
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}

ThreadLocal爲何會內存泄漏

一張圖來看一下ThreadLocal對象以及和其相關的引用:

能夠看出,以用有兩個引用
ThreadLocal ---> 堆對象
Current Thread ---> Thread ---> ThreadLocalMap ---> Entry ---> ThreadLocal ---> 堆對象

ThreadLocalMap使用ThreadLocal的弱引用做爲key,若是一個ThreadLocal沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,若是當前線程再遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠沒法回收,形成內存泄漏。其實,ThreadLocalMap的設計中已經考慮到這種狀況,也加上了一些防禦措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap裏全部key爲null的value。可是這些被動的預防措施並不能保證不會內存泄漏:

  • 使用static的ThreadLocal,延長了ThreadLocal的生命週期,可能致使的內存泄漏。

  • 分配使用了ThreadLocal又再也不調用get(),set(),remove()方法,那麼就會致使內存泄漏。

ThreadLocal 最佳實踐

綜合上面的分析,咱們能夠理解ThreadLocal內存泄漏的來龍去脈,那麼怎麼避免內存泄漏呢?
每次使用完ThreadLocal,都調用它的remove()方法,清除數據。
在使用線程池的狀況下,沒有及時清理ThreadLocal,不只是內存泄漏的問題,更嚴重的是可能致使業務邏輯出現問題。因此,使用ThreadLocal就跟加鎖完要解鎖同樣,用完就清理。

總結

  • ThreadLocal 不是用於解決共享變量的問題的,也不是爲了協調線程同步而存在,而是爲了方便每一個線程處理本身的狀態而引入的一個機制。這點相當重要。
  • 每一個Thread內部都有一個ThreadLocal.ThreadLocalMap類型的成員變量,該成員變量用來存儲實際的ThreadLocal變量副本。
  • ThreadLocal並非爲線程保存對象的副本,它僅僅只起到一個索引的做用。它的主要木得視爲每個線程隔離一個類的實例,這個實例的做用範圍僅限於線程內部。
  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據,避免形成內存泄露。
相關文章
相關標籤/搜索