Java基礎進階之ThreadLocal詳解

ThreadLocal基本在項目開發中基本不會用到, 可是面試官是比較喜歡問這類問題的;因此仍是有必要了解一下該類的功能與原理的.

ThreadLocal是什麼

ThreadLocal是一個將在多線程中爲每個線程建立單獨的變量副本的類; 當使用ThreadLocal來維護變量時, ThreadLocal會爲每一個線程建立單獨的變量副本, 避免因多線程操做共享變量而致使的數據不一致的狀況;java

ThreadLocal類用在哪些場景

通常來講, ThreadLocal在實際工業生產中並不常見, 可是在不少框架中使用卻可以解決一些框架問題; 好比Spring中的事務、Spring 中 做用域 ScopeRequest的Bean 使用ThreadLocal來解決.面試

ThreadLocal使用方法

一、將須要被多線程訪問的屬性使用ThreadLocal變量來定義; 下面以網上多數舉例的DBConnectionFactory類爲例來舉例sql

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBConnectionFactory {

    private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            try {
                return DriverManager.getConnection("", "", "");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

    public Connection getConnection() {
        return dbConnectionLocal.get();
    }
}

這樣在Client獲取Connection的時候, 每一個線程獲取到的Connection都是該線程獨有的, 作到Connection的線程隔離; 因此並不存在線程安全問題數組

ThreadLocal如何實現線程隔離

一、主要是用到了Thread對象中的一個ThreadLocalMap類型的變量threadLocals, 負責存儲當前線程的關於Connection的對象, 以dbConnectionLocal 這個變量爲Key, 以新建的Connection對象爲Value; 這樣的話, 線程第一次讀取的時候若是不存在就會調用ThreadLocalinitialValue方法建立一個Connection對象而且返回;緩存

具體關於爲線程分配變量副本的代碼以下:安全

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

一、首先獲取當前線程對象t, 而後從線程t中獲取到ThreadLocalMap的成員屬性threadLocals數據結構

二、若是當前線程的threadLocals已經初始化(即不爲null) 而且存在以當前ThreadLocal對象爲Key的值, 則直接返回當前線程要獲取的對象(本例中爲Connection);多線程

三、若是當前線程的threadLocals已經初始化(即不爲null)可是不存在以當前ThreadLocal對象爲Key的的對象, 那麼從新建立一個Connection對象, 而且添加到當前線程的threadLocals Map中,並返回併發

四、若是當前線程的threadLocals屬性尚未被初始化, 則從新建立一個ThreadLocalMap對象, 而且建立一個Connection對象並添加到ThreadLocalMap對象中並返回。框架

若是存在則直接返回很好理解, 那麼對於如何初始化的代碼又是怎樣的呢?

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

一、首先調用咱們上面寫的重載事後的initialValue方法, 產生一個Connection對象

二、繼續查看當前線程的threadLocals是否是空的, 若是ThreadLocalMap已被初始化, 那麼直接將產生的對象添加到ThreadLocalMap中, 若是沒有初始化, 則建立並添加對象到其中;

同時, ThreadLocal還提供了直接操做Thread對象中的threadLocals的方法

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

這樣咱們也能夠不實現initialValue, 將初始化工做放到DBConnectionFactorygetConnection方法中:

public Connection getConnection() {
    Connection connection = dbConnectionLocal.get();
    if (connection == null) {
        try {
            connection = DriverManager.getConnection("", "", "");
            dbConnectionLocal.set(connection);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    return connection;
}

那麼咱們看過代碼以後就很清晰的知道了爲何ThreadLocal可以實現變量的多線程隔離了; 其實就是用了Map的數據結構給當前線程緩存了, 要使用的時候就從本線程的threadLocals對象中獲取就能夠了, key就是當前線程;

固然了在當前線程下獲取當前線程裏面的Map裏面的對象並操做確定沒有線程併發問題了, 固然能作到變量的線程間隔離了;

如今咱們知道了ThreadLocal究竟是什麼了, 又知道了如何使用ThreadLocal以及其基本實現原理了是否是就能夠結束了呢? 其實還有一個問題就是ThreadLocalMap是個什麼對象, 爲何要用這個對象呢?

ThreadLocalMap對象是什麼

本質上來說, 它就是一個Map, 可是這個ThreadLocalMap與咱們平時見到的Map有點不同

一、它沒有實現Map接口;

二、它沒有public的方法, 最多有一個default的構造方法, 由於這個ThreadLocalMap的方法僅僅在ThreadLocal類中調用, 屬於靜態內部類

三、ThreadLocalMap的Entry實現繼承了WeakReference<ThreadLocal<?>>

四、該方法僅僅用了一個Entry數組來存儲Key, Value; Entry並非鏈表形式, 而是每一個bucket裏面僅僅放一個Entry;

要了解ThreadLocalMap的實現, 咱們先從入口開始, 就是往該Map中添加一個值:

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

先進行簡單的分析, 對該代碼表層意思進行解讀:

一、看下當前threadLocal的在數組中的索引位置 好比: `i = 2`, 看 `i = 2` 位置上面的元素(Entry)的`Key`是否等於threadLocal 這個 Key, 若是等於就很好說了, 直接將該位置上面的Entry的Value替換成最新的就能夠了;

二、若是當前位置上面的 Entry 的 Key爲空, 說明ThreadLocal對象已經被回收了, 那麼就調用replaceStaleEntry

三、若是清理完無用條目(ThreadLocal被回收的條目)、而且數組中的數據大小 > 閾值的時候對當前的Table進行從新哈希

因此, 該HashMap是處理衝突檢測的機制是向後移位, 清除過時條目 最終找到合適的位置;

瞭解完Set方法, 後面就是Get方法了:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

先找到ThreadLocal的索引位置, 若是索引位置處的entry不爲空而且鍵與threadLocal是同一個對象, 則直接返回; 不然去後面的索引位置繼續查找;

使用ThreadLocal形成內存泄露

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadLocalDemo {
    static class LocalVariable {
        private Long[] a = new Long[1024 * 1024];
    }

    // (1)
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    // (2)
    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

    public static void main(String[] args) throws InterruptedException {
        // (3)
        Thread.sleep(5000 * 4);
        for (int i = 0; i < 50; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    // (4)
                    localVariable.set(new LocalVariable());
                    // (5)
                    System.out.println("use local varaible" + localVariable.get());
                    localVariable.remove();
                }
            });
        }
        // (6)
        System.out.println("pool execute over");
    }
}

我在網上找到一個樣例, 若是用線程池來操做ThreadLocal 對象確實會形成內存泄露, 由於對於線程池裏面不會銷燬的線程, 裏面總會存在着<ThreadLocal, LocalVariable>的強引用, 由於final static 修飾的 ThreadLocal 並不會釋放, 而ThreadLocalMap 對於 Key 雖然是弱引用, 可是強引用不會釋放, 弱引用固然也會一直有值, 同時建立的LocalVariable對象也不會釋放, 就形成了內存泄露; 若是LocalVariable對象不是一個大對象的話, 其實泄露的並不嚴重, 泄露的內存 = 核心線程數 * LocalVariable對象的大小;

因此, 爲了不出現內存泄露的狀況, ThreadLocal提供了一個清除線程中對象的方法, 即 remove, 其實內部實現就是調用 ThreadLocalMapremove方法:

private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

找到Key對應的Entry, 而且清除Entry的Key(ThreadLocal)置空, 隨後清除過時的Entry便可避免內存泄露;

相關文章
相關標籤/搜索