聊聊对ThreadLocal的理解


JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。也是面试中出现频率比较高的知识点。ThreadLocal望文生义即表示的是一个线程中的局部变量,对比我们自己编写业务类中的私有变量,在对象实例化后,类的私有属性就是实例对象的局部变量,在实例对象的整个生命周期都可以随时获取这些私有变量。那么ThreadLocal和我们自己编写业务类的私有变量有什么本质区别吗?实际上没有区别,ThreadLocal的中存储的线程局部变量,最终也是Thread类中的一个局部变量(类似于Map的数据结构),生命周期和Thread实例一样。只不过JDK通过ThreadLocal做了一层封装,并对外提供了更简便的API。下面就来看看ThreadLocal的实现原理,它到底是做了什么事情。

ThreadLocal的应用

ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。因此,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也是访问不到的。通过ThreadLocal.set()保存到当前线程实例的ThreadLocalMap对象中,ThreadLocal实例是作为map的key来使用的。 下面来看一个hibernate中典型的ThreadLocal的应用:

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

由于在数据库操作中,很多地方会用到Session会话,而且操作都是在同一个线程中。所以hibernate把Session存入到ThreadLocal中,以便在其他方法获取Session实例时,直接通过ThreadLocal获取。这里如果不用ThreadLocal方式,该怎么做呢?最直接的方式,就是把需要用到Session对象的方法多加一个Session类型的入参。但这个种方式很麻烦,而且还有一个不好的地方。因为框架封装了数据库的缘故,我们编写的dao层代码是通过AOP嵌入到hibernate中执行的,因此我们编写dao层的代码就可能需要加上Session参数而我们又完全用不上这个参数,这会让开发者感到莫名其妙。hibernate利用ThreadLocal得到了一个很完美的设计思路。不仅仅是hibernate,在其他许多框架中都可以看到ThreadLocal的身影

ThreadLocal的原理

话不多说了吧,直接上源码,主要来看一下ThreadLocal的get和set方法,这两个方法是最常用的

    /**
     * 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) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }


    /**
     * 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);
    }

其中set方法中又通过getMap方法获取数据,返回的是一个ThreadLocalMap,我们看一下getMap的实现

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

在getMap方法中,通过Thread对象直接获取了threadLocals属性,再来看一下Thread类中的threadLocals属性

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

注意threadLocals没有关键字修饰,表示它是default的,表示该属性只允许在同一个包中进行访问。到这里就明白了,在每个Thread线程被创建的时候,线程对象本身就包含一个ThreadLocalMap类型的属性,用来存储线程局部变量。我们再来看一看ThreadLocalMap是个什么数据结构,其实看名字也能知道它是个Map。但它和HashMap有一点区别,就是对于哈希冲突的处理方式。ThreadLocalMap实现更简单轻巧,但不适合存储大量的数据。后面会再详细说这一点。下面是运行时ThreadLocal的数据结构图

线程运行时,内部保存着一个Map的数据结构,而Map的Key是固定的ThreadLocal类型,值就是存储我们自己想存储的数据,图中用Object类型表示。也就是说 ThreadLocal本身并不存储值,它只是作为一个 key来让线程从 ThreadLocalMap获取 value。看明白了这个图,就明白了ThreadLocal的工作原理了。

哈希冲突

前面说过ThreadLocalMap实现方式简单,不适合存储大量的数据,那么ThreadLocalMap是怎么实现的呢,它是怎么解决哈希冲突的。这就是需要深入看一下set方法的源码

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        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();
        }

到这里看着和HashMap类似,也定义了一个Entry数组。首先通过当前key的哈希码和数组长度,计算出当前key应该存储在数组中的下标位置,继续看下面for循环,这个逻辑很简单,从数组中获取刚刚算出的下标位置的元素,如果为空就直接把key和value存储进去,如果不为空就出现哈希碰撞了,它是如何解决的。重点在于nextIndex方法。继续看nextIndex方法的源码。

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

是不是很震惊,这个方法竟然如此的简单。就是把下标往后移一位,然后返回,再继续刚刚的循环。由此可见,ThreadLocalMap处理哈希碰撞的方式就是把下标往后移一位。直到能放进去为止。这也是它为什么不适合存储大量数据的原因,数据一多出现哈希碰撞的概率就大,如果出现大量的哈希碰撞,那么数组就会特别长。同样在get方法的时候,会先根据key计算出下标,再比较key是否相等。如果不相等就从计算出的下标开始往后遍历数组。源码如下

        /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        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);
        }


        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

getEntry就是从数组中获取Entry实例,当出现key不相等时会进入else,也就是getEntryAfterMiss方法。从这个方法可以看出,出现哈希碰撞就会遍历数组了。如果看过HashMap源码的朋友,看这个代码应该不在话下,逻辑非常简单明了。

内存泄漏

ThreadLocal会出现内存泄漏,其原因出现在Entry中的Key是一个弱引用。源码如下

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


   }

这里省略了一些无关代码,Entry是ThreadLocalMap的静态内部类。从代码中可以看出Entry的Key是WeakReference包装的,是一个弱引用类型。什么是弱引用,在Java中有4中引用:强引用,弱引用, 虚引用, 软引用。这里只说弱引用,我们平时写代码基本使用的都是强引用。


// 强引用
String str = "hello";

// 弱引用
WeakReference<String> weak = new WeakReference<String>(new String("hello"));

// 主动触发GC
System.gc();

弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。因此一个对象如果只有弱引用,那么它能存活到下一次GC之前。在ThreadLocalMap的设计中,Key使用的是弱引用,那么在运行时,引用链如下:

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,当然这只是一种防护措施,JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
深入Mybatis源码实现 深入Mybatis源码实现
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和
2019-07-06
Next 
深入Java线程池实现源码 深入Java线程池实现源码
Java线程池是使用频率很高的开源框架。也是在面试中常被问到的组件。它的实现源码在J.U.C包下,本人也经常使用线程池,简单方便。大多是浮于表面的一些API的调用,对于框架实现中具体做了哪些事情,却是知之甚少。本文将从源码角度,深入了聊一聊
2019-06-11
  TOC