Java-ThreadLocal

352 阅读7分钟

什么是ThreadLocal?

声明:本文使用的是JDK 1.8

首先我们来看一下JDK的文档介绍:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 *
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 **/

结合我的总结可以这样理解:ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal, 每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

ThreadLocal源码分析

ThreadLocal原理

ThreadLocal类:

public class ThreadLocal<T> { 
        // 构造器 
        public ThreadLocal() {} 

        //返回此线程的当前线程“初始值”,线程局部变量。此方法将在第一次调用时调用
        protected T initialValue() {} 

        // 创建ThreadLocalMap,ThreadLocal底层其实就是一个map来维护的。 
        void createMap(Thread t, T firstValue) {} 

        // 返回该当前线程对应的线程局部变量值。 
        public T get() {} 

        // 获取ThreadLocalMap 
        ThreadLocalMap getMap(Thread t) {} 

        // 设置当前线程的线程局部变量的值 
        public void set(T value) {} 

        /**将当前线程局部变量的值删除,目的是为了减少内存占用。
        其实当线程结束后对应该线程的局部变量将自动被垃圾回收,
        所以无需我们调用remove,我们调用remove无非也就是加快内存回收速度。**/
        public void remove() {} 

        // 设置初始值,调用initialValue 
        private T setInitialValue() {} 

        // 静态内部类,一个map来维护的 
        static class ThreadLocalMap { 
            // ThreadLocalMap的静态内部类,继承了弱引用,这正是不会造成内存泄漏根本原因 
            // Entry的key为ThreadLocal并且是弱引用。value是值 
            static class Entry extends WeakReference<ThreadLocal<?>> {
                ......
            } 
        } 
        ......
}

首先,来看一下ThreadLocal的set()方法

public void set(T value) {
    // 得到当前线程对象
    Thread t = Thread.currentThread();
    // 这里获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
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是ThreadLocal的一个内部类。用Entry类来进行存储

我们的值都是存储到这个Map上的,key是当前ThreadLocal对象

如果该Map不存在,则初始化一个:

/**
Create the map associated with a ThreadLocal. Overridden in InheritableThreadLocal.
Params:
t – the current thread
firstValue – value for the initial entry of the map
**/
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

如果该Map存在,则从Thread中获取:

/**
Get the map associated with a ThreadLocal. Overridden in InheritableThreadLocal.
Params:
t – the current thread
Returns:
the map
**/
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

Thread维护了ThreadLocalMap变量:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

综上,ThreadLocalMap是在ThreadLocal中使用内部类来编写的,但对象的引用是在Thread中。

于是得出:Thread为每个线程维护了ThreadLocalMap的一个Map,而ThreadLocalMapkeyThreadLocal对象本身,value则是要存储的对象。

有了上面的基础,我们看get()方法就一点都不难理解了:

/** 
* 获取当前线程下的entry里的value值。 
* 先获取当前线程下的ThreadLocalMap, 
* 然后以当前ThreadLocal为key取出map中的value 
*/ 
public T get() { 
        // 获取当前线程 
        Thread t = Thread.currentThread(); 
        // 获取当前线程对应的ThreadLocalMap对象。 
        ThreadLocalMap map = getMap(t); 
        // 若获取到了。则获取此ThreadLocalMap下的entry对象,若entry也获取到了,那么直接获取entry对应的value返回即可。 
        if (map != null) { 
            // 获取此ThreadLocalMap下的entry对象 
            ThreadLocalMap.Entry e = map.getEntry(this); 
            // 若entry也获取到了 
            if (e != null) { 
                @SuppressWarnings("unchecked") 
                // 直接获取entry对应的value返回。 
                T result = (T)e.value; 
                return result; 
             } 
        } 
        // 若没获取到ThreadLocalMap或没获取到Entry,则设置初始值。 
        // 知识点:初始值方法是延迟加载,只有在get才会用到,只有在这获取没获取到才会初始化,下次就肯定有值了,所以只会执行一次
        return setInitialValue(); 
}

总结

  1. 每个Thread维护着一个ThreadLocalMap的引用
  2. ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
  3. 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
  4. 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
  5. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。 所以ThreadLocal能够实现数据隔离,获取当前线程的局部变量值,不受其他线程影响。

ThreadLocal里的对象线程安全吗?

未必,如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()获取的还是这个共享对象本身,还是有并发访问线程不安全问题。

ThreadLocal内存泄漏问题,在线程池中又是如何?

问题

先分析一下:

  • ThreadLocalMap.Entry的key会内存泄漏吗?
  • ThreadLocalMap.Entry的value会内存泄漏吗?

先看下key-value的核心源码

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

先看继承关系,发现是继承了弱引用,而且key直接是交给了父类处理super(key),父类是个弱引用,所以key完全不存在内存泄漏问题,因为他不是强引用,它可以被GC回收的。

弱引用的特点:如果这个对象只被弱引用关联,没有任何强引用关联,那么这个对象就可以被GC回收掉。弱引用不会阻止GC回收。这是jvm知识。

再看value,发现value是个强引用,但是想了下也没问题的呀,因为线程终止了,不管是强引用还是弱引用,都会被GC掉的,因为引用链断了(jvm用的可达性分析法,线程终止了,根节点就断了,下面的都会被回收)。

这么分析好像并没有什么问题,那么在线程池呢?线程池的存在核心线程是不会销毁的,只要创建出来他会反复利用,生命周期不会结束掉,但是key是弱引用会被GC回收掉,value强引用不会回收,所以形成了如下场面:

Thread->ThreadLocalMap->Entry(key为null)->value

由于value和Thread还存在链路关系,还是可达的,所以不会被回收,这样越来越多的垃圾对象产生却无法回收,迟早内存泄漏,时间久了必定OOM。

那么解决方案ThreadLocal已经为我们想好了,提供了remove()方法,这个方法是将value移出去的。所以用完后记得remove()

避免内存泄露

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。 想要避免内存泄露就要手动remove()掉

ThreadLocal使用场景

最常见的ThreadLocal使用场景为用来解决数据库连接Session管理等。

数据库连接:

private static ThreadLocal<Connection> connectionHolder=new ThreadLocal<Connection>() {
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};
 
public static Connection getConnection() {
    return connectionHolder.get();
}

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

声明

ps:文章如有错误麻烦请告知,另外此文章仅用于学习,参考了以下文章,如冒犯了其中利益请告知我删除:
ThreadLocal-知乎