多线程的ThreadLocal类(一):ThreadLocal概览以及通过源码分析运行原理

前言:

本文作为ThreadLocal类的第一篇文章,主要通过源码分析ThreadLocal类的实现原理,概括它的运行方式,并简单分析如何使用ThreadLocal。

1、ThreadLocal类概述

1.1、什么是ThreadLocal?

了解源码的第一步应当优先解读类文件的顶部注释。

/**
 * 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).
 *
 * 

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. *

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

Each thread holds an implicit reference to its copy of a thread-local * variable as long as the thread is alive and the {@code ThreadLocal} * instance is accessible; after a thread goes away, all of its copies of * thread-local instances are subject to garbage collection (unless other * references to these copies exist). * * @author Josh Bloch and Doug Lea * @since 1.2 */

从顶部注释中,我们可以得到:ThreadLocal提供的变量与普通变量不同。ThreadLocal提供了线程的局部变量,每个线程都可以通过get()和set()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,从而在多线程的情况下实现了线程的数据隔离

简要言之:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

1.2、为什么要学习ThreadLocal类?

回想一下生活中的一些常见的例子,例如浏览器访问网站时,会根据我们访问的网站从浏览器本地缓存中寻找该网站对应的Cookie,并且将这个Cookie包含在请求头里发送过去,而不是使用者去浏览器缓存中寻找A网站的Cookie,从而避免了参数传递。并且不同的浏览器之间的数据相互不可见,例如谷歌浏览器缓存中存储的Cookie火狐浏览器是不可见的,当然也不可能火狐浏览器打开了A网站,然后在谷歌浏览器中找到A网站的Cookie并放在请求头里发过去,从而实现了数据隔离。

在这个场景下,浏览器充当了一个类似中介的角色,我们需要什么,例如我在谷歌浏览器打开了A网站,就去和谷歌浏览器这个“中介”说一声:“把A网站的Cookie给我”,然后就能拿到A网站的Cookie,而不是自己再去手动的一个一个寻找Cookie。

再举一个例子,我们生活中常用的丰巢快递柜。如果快递小哥把我们网购的快递放到了丰巢快递柜,那么我们去拿快递的时候,只要找到对应的快递柜,手机扫码,之后快递柜就会自动打开我们的快递存放的那个仓位的仓门,不需要我们去一个仓位一个仓位的寻找快递。如果找错了快递柜,相应的扫码之后就会直接给出提示。

(例子如果举的有不恰当之处,欢迎举正)

在多线程的编程中,ThreadLocal也正是充当了这个中介的角色,在多线程中调用ThreadLocal的get()和set()方法,就能根据当前的线程直接存放或者取出对应的值,无需在做额外的操作,在保证了线程间数据隔离的前提下避免了参数传递。

2、ThreadLocal使用示例

public class Main {

    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        for (int i=0; i<5; ++i) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        threadLocal.set(Thread.currentThread().getName());
                        System.out.println(Thread.currentThread().getName() + " is stored");
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getId() + ":" + threadLocal.get());
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                }
            }).start();
        }
    }

}

运行结果:

Thread-0 is stored
Thread-2 is stored
Thread-3 is stored
Thread-1 is stored
Thread-4 is stored
13:Thread-2
14:Thread-3
15:Thread-4
11:Thread-0
12:Thread-1

Process finished with exit code 0

3、ThreadLocal的实现原理

想要知道ThreadLocal的实现原理,就要去阅读它的源码。本文以JDK1.8为例,分析ThreadLocal的源码。其中,get()和set()是ThreadLocal最常用的两个方法。

ThreadLocal设计的奇妙之处在于:ThreadLocal变量本身并不保存数据,数据都是存储在当前线程维护的ThreadLocalMap中ThreadLocalMap作为一个静态内部类,定义在ThreadLocal类中,当前线程初始化并维护这个ThreadLocalMap,而ThreadLocal变量只是充当了一个key,需要取值时,ThreadLocal变量作为key(实际上key并不是ThreadLocal本身,而是它的一个弱引用)从线程私有的ThreadLocalMap中取值。由于每个线程的ThreadLocalMap都是私有的,所以ThreadLocal能很好的实现线程间的数据隔离。

由于ThreadLocal的操作都是基于ThreadLocalMap进行的,因此,在阅读ThreadLocal源码之前,我们先去看看ThreadLocalMap的实现。

3.1、ThreadLocalMap

先看看ThreadLocalMap的顶部注解。

    /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */

 从注解中,我们可以知道:

  • ThreadLocalMap是一个专门根据ThreadLocal定制的hash散列映射,与普通的HashMap不同,它只能用来保存线程本地变量;
  • 它的所有操作只能通过ThreadLocal实例调用相应的方法完成;
  • 哈希表中的key使用的是弱引用,目的是为了处理大量长时间存活的引用,并且只有当哈希表的空间耗尽的时候才会开始删除过期的键值对。
        /**
         * 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> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

 ThreadLocalMap哈希表中键值对的定义,key是ThreadLocal对象的弱引用,因此当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”永远无法回收,造成内存泄露。

虽然,JDK调用ThreadLocalMap的getEntry()函数或者set()函数,会清除这些null key的entry,但是还不够。因为可能后面不调用这些函数了,所以这种方式不可能任何情况都能解决。

因此,很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

如果线程迟迟无法结束,也就是ThreadLocal对象将一直不会回收,例如线程池的情况,那么也将导致内存泄漏。(内存泄露的重点)。

 

3.2、ThreadLocal的常用方法

接下来看一下ThreadLocal最常见的几个方法。

3.2.1、构造方法

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

 

3.2.2、set()方法

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

根据注释,可以知道这个方法大概做了什么事:把这个线程局部变量中,当前线程副本的值设置为指定的值。

  • 获取当前线程;
  • 找到当前线程中的ThreadLocalMap;
  • 如果当前线程的ThreadLocalMap还没有初始化,初始化ThreadLocalMap的同时设置数据,否则根据当前线程设置对应的key-value。

 set()方法中涉及到了getMap()、set()、createMap()。

getMap():

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

作用:输入当前的线程对象,返回对应的ThreadLocalMap(Thread类中定义的ThreadLocalMap变量名是threadLocals)。

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

作用:根据哈希算法,在当前线程对象维护的ThreadLocalMap中找到对应的位置,并插入键值对(具体的运行原理在下一遍ThreadLocal的文章中再详细描述)。

createMap():

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

作用:在线程中维护的ThreadLocalMap 还未初始化(第一次调用set()方法)时触发,实例化一个ThreadLocalMap 对象赋给当前线程中的threadLocals变量,并插入第一个键值对。

3.2.3、get()方法

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

 根据注释,结合我个人的理解,可以知道:返回此线程局部变量的当前线程副本中的值。如果变量没有当前线程的值,则首先将其初始化为调用{@link#initialValue}方法返回的值。

3.2.4、remove()

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

作用:如果Thread对象维护的ThreadLocalMap不为null,则删除key对应的键值对。

参考资料:

JDK1.8源码

你可能感兴趣的:(Java基础,java)