ThreadLocal线程局部变量详解

目录

  • 简介

  • 使用示例

  • ThreadLocal原理

  • 避免内存泄漏

  • 小结

参考:https://www.cnblogs.com/zhangjk1993/archive/2017/03/29/6641745.html*
https://www.cnblogs.com/xzwblog/p/7227509.html
*Java3y【对线面试官】ThreadLocal

简介

首先明确一个概念,那就是ThreadLocal并不是用来并发控制某个共享变量的,而是为了给每个线程分配一个只属于该线程的变量,顾名思义它是local variable(线程局部变量)。它的功能非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,每个线程都可以通过 set()get() 来独立地改变自己的副本,而不会和其它线程的副本冲突,实现了线程之间的数据隔离。

ThreadLocal往往作为一个private static变量,来关联一个线程的状态 (比如user ID 或 Transaction ID)。例如,下面这个类为每个线程生成一个唯一标识符,每个线程的id在它第一次调用ThreadId.get() 时创建,然后对同一个线程而言,后续的每次调用所获得的都是同一个id。

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
            new ThreadLocal<Integer>() {
                @Override
                protected Integer initialValue() {
                    return nextId.getAndIncrement();
                }
            };

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }
}

每个线程在整个生命周期内都隐式地维持着一个线程局部变量的拷贝。在线程终止后,所有与之关联的线程局部变量会被GC回收(除非还有其他地方持有着这些变量的引用)。

使用示例

ThreadLocal最常见的应用场景是,管理Connection连接、管理Session会话 等。

下面是一个自定义事务管理器的例子:

public class JdbcUtils {
    private static DruidDataSource dataSource;
    private static final ThreadLocal<Connection> CONNECTION_THREAD_LOCAL = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            try {
                DruidPooledConnection connection = dataSource.getConnection();
                connection.setAutoCommit(false); // 关闭自动提交
                return connection;
            } catch (SQLException throwables) {
                throwables.printStackTrace();
                System.err.println("获取连接失败...");
                return null;
            }
        }
    };

    static {
        Properties properties = new Properties();
        try {
            InputStream in = JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
            properties.load(in);
            dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 从数据库连接池获取连接 (默认关闭了自动提交)
     * @return 若返回null, 说明获取连接失败
     */
    public static Connection getConnection() {
        return CONNECTION_THREAD_LOCAL.get();
    }

    /** 提交事务, 关闭连接 */
    public static void commitAndClose() {
        try (Connection connection = CONNECTION_THREAD_LOCAL.get()) {
            connection.commit();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            // 必须移除当前线程的连接, 原因是:
            // Tomcat底层使用了线程池技术, 导致线程被复用, 获取到已经关闭的连接
            // 准确地说是获取到已经释放回Druid连接池的连接, 如果使用了这样的连接, 就会报错
            CONNECTION_THREAD_LOCAL.remove();
        }
    }

    /** 回滚事务, 关闭连接 */
    public static void rollbackAndClose() {
        try (Connection connection = CONNECTION_THREAD_LOCAL.get()) {
            connection.rollback();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }finally {
            // 移除当前线程的连接
            CONNECTION_THREAD_LOCAL.remove();
        }
    }

}

下面是Hibernate中一个典型的管理Session的例子:

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

ThreadLocal原理

set()get()方法是ThreadLocal类中最常用的两个方法,下面看看这两个方法的内部实现。

先来看下 set() 方法:设置一个与当前线程关联的变量值。

// 设置一个与当前线程关联的变量值。同一个线程多次调用,会覆盖先前设置的值。
// 如果懒得 set() 初始值, 你可以重写 initialValue() 来指定第一次 get() 时获取到的初始值。
public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 取出当前线程内部的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 若map非空, 则以当前ThreadLocal对象本身(this)为键, 将value设置进map中
    // 若map为空, 则先创建map, 然后再将value设置进去
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

// 获得当前线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 初始化当前线程的ThreadLocalMap, 并立马设置一个firstValue
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

getMap() 方法做的事情很简单,就是返回当前线程即Thread对象内的threadLocals成员变量,类型是ThreadLocalMap。而我们往ThreadLocal中存的value值都被放到了这个map中,所以ThreadLocal对象本身是不存储任何数据的,它只是作为一个key,一个「能够定位到Thread里的ThreadLocalMap里面的value值」的key。

public class Thread implements Runnable {
    // 与当前线程相关联的所有  都在这个 map 中
    // 这个 map 的引用会被 ThreadLocal 的 get() 获取
    ThreadLocal.ThreadLocalMap threadLocals = null;

需要注意的是,由于每个Thread对象都只能访问自己的ThreadLocalMap,这意味着多个线程之间的ThreadLocalMap是相互隔离的。这样也就实现了一个ThreadLocal实例对于不同的线程,会定位到不同的ThreadLocalMap,进而再以ThreadLocal实例本身作为key,从map中取出对应的value值。也就是这么一个查找逻辑:ThreadThreadLocalMapEntry

另外,ThreadLocalMap其实是ThreadLocal的内部类,每个key用一个弱引用来存储。但是,这个map对象并不是在ThreadLocal里使用,而是由Thread维护的,这样才能实现线程隔离的map嘛。

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

综上,ThreadLocalThreadThreadLocalMap之间,大概是这么一个关联关系:

ThreadLocal线程局部变量详解_第1张图片

ok,理解了 set() 方法后,现在看 get() 方法就容易多了:获取与当前线程关联的变量值。

// 获取当前线程关联的变量值。
// 如果该变量值为空 (first invoke or invoke after remove()),则调用 initialValue() 初始化它。
// 子类可以重写 initialValue() 方法, 来指定第一次 get() 时获取到的初始值。
public T get() {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 取出当前线程内部的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 以当前ThreadLocal对象本身 (this) 为键, 取出对应的Entry, 返回它的value值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

// 为当前线程设置一个初始值
private T setInitialValue() {
    // 创建初始值, 默认是null
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 若map非空, 则以当前ThreadLocal对象本身(this)为键, 将value设置进map中
    // 若map为空, 则先创建map, 然后再将value设置进去
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    // 返回设置好的value值
    return value;
}

/**
 * Returns the current thread's "initial value" for this
 * thread-local variable.  This method will be invoked the first
 * time a thread accesses the variable with the {@link #get}
 * method, unless the thread previously invoked the {@link #set}
 * method, in which case the {@code initialValue} method will not
 * be invoked for the thread.  Normally, this method is invoked at
 * most once per thread, but it may be invoked again in case of
 * subsequent invocations of {@link #remove} followed by {@link #get}.
 *
 * 

This implementation simply returns {@code null}; if the * programmer desires thread-local variables to have an initial * value other than {@code null}, {@code ThreadLocal} must be * subclassed, and this method overridden. Typically, an * anonymous inner class will be used. * * @return the initial value for this thread-local */ protected T initialValue() { return null; }

简单来说,就是以ThreadLocal对象即this作为key,从当前线程ThreadThreadLocalMap中取出对应的value值。若该valuenull,则调用initialValue() 初始化一个值并放入ThreadLocalMap中,然后返回它。所以,子类可以重写 initialValue() 方法,来指定第一次 get() 时获取到的初始值,不指定的话则默认返回null

避免内存泄漏

前面我们知道了每个Thread都有一个ThreadLocalMap,该map的key是一个ThreadLocal实例。和传统的HashMap源码解读(put/get/resize)类似,键值对也是被包在一个Entry里面,该Entry的定义如下:

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是一个弱引用 (WeakReference) —— GC时回收,不过弱引用针对的只是键值对中的key而已。每个传入的key(Threadlocal对象)都会被封装成一个WeakReference

根据弱引用的特性,可以得知:对于任意一个ThreadLocal对象,如果整个JVM中不存在任何指向它的强引用和软引用,那么下一次GC时该对象就会被回收,从而ThreadLocalMap中的这个key,或者说这个key也就是WeakReference里的referent就会变成null

同样的,我们希望当key被回收掉时,对应的value也被GC回收掉。然而,事实是value无法和key一起被回收,可能会导致内存泄漏(Memory Leak)。因为总是存在这样一条强引用链:ThreadThraedLocalMapEntryvalue,导致value的生命周期变得和Thread一样长,也就是说只有当线程被销毁时,该线程的ThraedLocalMap里的所有value才会被回收。

更糟糕的是,如果项目里使用了线程池,一个线程可能长期占据于内存中,导致不使用的value在很长一段时间内不会被回收,那么value这部分内存就白白浪费了,从而引发内存泄漏。

为了防止内存泄漏,ThraedLocalMapset() / getEntry() / remove() 中额外做了一些事情:

  • set():通过线性探测法查找元素,如果查找过程中碰到key为nullEntry,说明之前这个位置的 ThreadLocal已经被回收了,则用新元素替代旧元素,并清理 i 到下一个 null slot 之间的所有 stale entry。当然,如果没出现哈希冲突,则直接将新元素放在对应位置上,则在对应位置上创建一个新的 Entry,然后尝试清理一些stale entry,最多清理 logn 个。

    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;
        // 根据 ThreadLocal 的 hash 值,确定对应元素在数组中的位置
        int i = key.threadLocalHashCode & (len - 1);
    
        // 通过线性探测法查找元素
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            ThreadLocal <?> k = e.get();
            // ThreadLocal 对应的 key 存在,直接覆盖之前的值
            if (k == key) {
                e.value = value;
                return;
            }
            // key 为 null,但是 value 不为 null,说明之前这个位置的 ThreadLocal 对象已经被回收了,当前数组中的 Entry 是一个陈旧(stale)的元素
            if (k == null) {
                // 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,以防止内存泄漏
                // 具体可以看源代码,没看太懂,大概是清理掉了 i 到下一个 null slot 之间的所有 stale entry
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 没出现哈希冲突,则在对应位置上创建一个新的 Entry
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // 尝试清理一些陈旧的 Entry(key == null),最多清理 logn 个,具体请参考源码。
        // 如果没有任何 Entry 被清除,则进行 rehash。
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    
  • getEntry():通过线性探测法查找元素,如果碰到非空且未被回收的Entry,直接返回即可;如果碰到key为nullEntry,说明之前这个位置的 ThreadLocal已经被回收了,则清理 i 到下一个 null slot 之间的所有 stale entry

    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        // 如果 Entry 非空且未被回收,直接返回
        if (e != null && e.refersTo(key))
            return e;
        else
            // 如果 Entry 为空或已被回收,进入该方法
            return getEntryAfterMiss(key, i, e);
    }
    
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
    
        while (e != null) {
            // 找到目标 Entry, 直接返回
            if (e.refersTo(key))
                return e;
            // 如果 Entry 已被回收,清理 i 到下一个 null slot 之间的所有 stale entry
            if (e.refersTo(null))
                expungeStaleEntry(i);
            else
                // 通过线性探测法继续查找
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }
    
  • remove():通过线性探测法查找要删除的元素,如果找到,则清理 i 到下一个 null slot 之间的所有 stale entry,这里i是要删除的Entry的下标。

    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.refersTo(key)) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }
    

以上只是列举了3个stale entry的清理时机,其实还有 rehash() 的时候,会对整张哈希表进行完整的扫描,一次性清理所有的stable entry。除了 rehash() 外,其他三个方法出于性能考虑,清理动作都采用「部分清理」,即只会清理一部分stale entry(从i 到下一个 null slot)。

但这显然是不够的,因为上面的设计思路依赖一个前提条件:在放入一个Entry之后,如果这个Entrykey被GC回收了,那么必须调用ThreadLocalMapset()getEntry()remove() 方法,才会将对应的value置为null,这时value才可以被GC回收,否则仍会造成内存泄漏。

最佳实践:

最稳妥的做法是,保证ThreadLocalset()remove() 是个成对操作,即set() 元素之后必须手动调用 remove() 删除元素,防止内存泄漏。同时,JDK建议将ThreadLocal变量定义成private static final的,只需要对其初始化一次就好了,没必要作为成员变量多次初始化。

public class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(nextId::getAndIncrement);

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }

    public static void remove() {
        threadId.remove();
    }
}

小结

简而言之,由于不同的线程有不同的ThreadLocalMap,这个map的key又正好是ThreadLocal,那么就使得同一个ThreadLocal可以间接对应到多个value值,(通过相互隔离的ThreadLocalMap对象实现)。但若以单个线程的视角来看,一个ThreadLocal其实只映射到一个value值,因为它看不到其他线程的ThreadLocalMap,自然就看不到其他value值了。

为了避免内存泄漏,我们要保证ThreadLocalset()remove() 是个成对操作。同时,JDK建议将ThreadLocal变量定义成private static final的,只需要对其初始化一次就好了。

你可能感兴趣的:(java)