ThreadLocal 源码详细解析

36 篇文章 0 订阅

ThreadLocal 源码详细解析

写这篇文章不在计划之内, 主要是分析到Handler消息机制一文中,牵涉到ThreadLocal内容,一次全部写完文章过长,所以打算单独摘出来梳理成为一篇文章。

引言

在我们日常开发中用到ThreadLocal的地方很多,在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。

核心部分

这里最主要的部分就是ThreaLcoal内部维护了一个HashMap, 下边我们看一下最主要的类ThreadLocalMap

ThreadLocal类

  • 每个线程有一个自己的 ThreadLocalMap 对象
  • 每一个 ThreadLocal 对象有一个创建时生成唯一的 Id
  • 访问一个 ThreadLocal 变量的值,就是用这个唯一 Id 去本线程的 ThreadLocalMap 中查找对应的值

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) {
        /// 可以看到在ThreadLocal创建的时候, 是获取到了当前线程t, 然后获取线程t的本地存储ThreadLcoalMap,然后对map进行操作。
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
        /// 当然如果当前线程还没有创建过ThreadLocalMap,则创建Map
            createMap(t, value); /// 创建Map 的过程 看下边ThreadLocalMap 中方法。
    }

get方法

接下来看一下另一个重要方法,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);
        /// 同样是获取到了线程内部的引用 map对象, 通过map内部getEntry方法 获取到该ThreadLocal对应的对象Entry 
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果没有, 默认为null, 所以在没有set 而且没有重写initialValue方法的话,获取到的值就是null。
        return setInitialValue();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            /// 如果还没有该map ,则使用当前初始值来创建。默认为null
            createMap(t, value);
        return value;
    }

     /**
     * 返回为当前线程创建的初始化值使用, 一般在get方法中调用和 remove方法后get调用, 
     * 如果使用可以通过内部类继承然后重写该方法即可。
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     * 获取到Thread的内部引用对象 threadLcoals
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

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)
            /// 这个方法就是通过map 的内部方法 remove 移除当前的值。 
             m.remove(this);
     }

ThreadLocal与Thread关系

说起ThreadLocal 与Thread的关系,借用Java编程思想中对线程本地存储的定义, 防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。 简单理解一下就是Thread上的LocalVariables了, 既然如此就进入Thread源码看一下。 很明显就找到了相关引用。


    /* Thread中引用了 ThreadLocal.ThreadLocalMap  很明显一线程中可以放置很多变量 , 这个Map可以在ThreadLocal中维护  */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal 可继承的ThreadLcoal
     这个Map可以在ThreadLocal中维护
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

主要用于在创建线程时继承当前线程的ThreadLocal values , 可以在Thread 的init中调用, 一般情况下,如果父类的可继承的ThreadLcoal.ThreadLcoalMap 指定过,并且不为空 则默认创建的子线程继承该变量。 具体创建方法就是ThreadLocal中的createInheritedMap方法。

  if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

看一下ThreadLcoal中的createInheritedMap

   /**
     * Factory method to create map of inherited thread locals.
     * Designed to be called only from Thread constructor.
     *
     * 仅仅只有在Thread 构造函数中调用
     * @param  parentMap the map associated with parent thread
     * @return a map containing the parent's inheritable bindings
     */
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

ThreadLocalMap

ThreadLocalMap 是ThreadLocal 的内部类, 既然起名为Map 则肯定和Map有相似之处,下边从get ,set ,remove方法来看。

基本结构

这是ThreadLcoalMap 的基本结构, 继承自弱引用WeakReference 使用了泛型ThreadLcoal

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

create 方法

这里几个方法是创建Map的过程, 列举出调用和最简单的一个构造函数。

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

    /**
     * Construct a new map initially containing (firstKey, firstValue).
     * ThreadLocalMaps are constructed lazily, so we only create
     * one when we have at least one entry to put in it.
     */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        /// 创建一个线性table , Entry 为item ,初始化INITIAL_CAPACITY 默认为16个
        table = new Entry[INITIAL_CAPACITY];
        /// 计算出第一个key的索引 i
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        /// 初始化 并设置size 值
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        /// 同时 设置容器大小的临界值,并传入初始化大小。
        setThreshold(INITIAL_CAPACITY);
    }

get 方法

    /**
     * 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.
     * /// 通过Key的hash值来创建其对应的索引值,找到entry 
     *
     * @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
            /// 如果没有在直接的table线性表中找到的话。
            return getEntryAfterMiss(key, i, e);
    }

可以看出它考虑到了哈希碰撞的情况,因为在遍历set值的时候考虑到哈希碰撞的问题一个节点对应两个值,一般会取(key的hashcode值&table.lenth-1)获取一个数组的位置,将其放入到该节点的位置。这里相当是一个逆运算,省去了遍历的性能开销问题,直接取该节点上的值(可以参考我以前的文章HashMap原理解析)。当然还有getEntryAfterMiss方法是为了解决出现了hash碰撞的问题,以下是getEntryAfterMiss方法:

    /**
     * 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;
                /// 如果k为空的直接从链表中擦除,方便GC进行回收
            if (k == null)
                expungeStaleEntry(i);
            else
            /// 循环找到下一个index
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

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;
          //如果数组中没有冗余的null值并且如果size大于临界值
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            /// 进行扩容 ,这里就不再进行解释如何扩容了。
            rehash();
    }

remove

    /**
     * Remove the entry for key.
     */
    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();
                // 置空,以便GC回收
                expungeStaleEntry(i);
                return;
            }
        }
    }

散列

理解了上边所说的ThreadLocalMap 的引用以及get和set, 这里就引出了个问题,如何保证每个线程中引用ThreadLcoalMap中创建的ThreadLcoal是唯一,并且高效的存取就成了至关重要的问题。
看到上边的ThreadLcoalMap中的set方法 和get方法, 如果懂得HashMap存储结构的话, 会发现和HashMap中indexfor (key的Hash& table.length-1) 有的一拼, 但是这里用的Hash key生成器不是ThreadLcoal对象的Hash值, 而是从0开始的AtomicInteger 通过getAndAdd HASH_INCREMENT 来生成。

   /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

这样就保证了每一个 ThreadLocal 对象有一个创建时生成唯一的 Id。

  • 为什么要引用这个0x61c88647奇奇怪怪的数字呢? 这个我也不知道,看注释就是为了让 hash code 能更好地分布在尺寸为 2 的 N 次方的数组里。 随后又网上搜了一下,有篇文章有点解释大概就是 表示的是黄金比例什么的,能够是生成的hashCode更加均匀的分布在2的N次方的数组里。 (英文二把刀见谅)
    如果有哪位数学高人,希望不吝赐教。
    这里放下这篇文章的链接https://www.javaspecialists.eu/archive/Issue164.html

总结

看了这么多,ThreadLcoal结构终于清晰了。 可以这么理解,ThreadLcoal是Thread中持有的一个放置Thread独有变量的Hash表。这个Hash表又经过特殊散列优化,使实用效率更加高效。可以说Java工程师们为了提高程序的结构性和高效性操碎了心啊,对于我们普通使用者来说简单方便而且又浑然一体,的确牛气。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值