JAVA并发-自问自答学ThreadLocalJava 之 ThreadLocal 详解。

前言

ThreadLocal诸多同室还将不亮是什么东西,可以就此来干嘛。但面试时却同时经常问到,所以这次我与豪门一齐学学ThreadLocal这个类。

脚我就算盖面试问答的样式学习我们的——ThreadLocal类似(源码分析基于JDK8)

1. 概念

ThreadLocal
用于提供线程局部变量,在差不多线程环境得以保证各个线程里的变量独立为外线程里之变量。也就是说
ThreadLocal 可以吧每个线程创建一个【单独的变量副本】,相当给线程的
private static 类型变量。

ThreadLocal
的企图以及协办机制有些相反:同步机制是为着保证多线程环境下多少的一致性;而
ThreadLocal 是确保了差不多线程环境下多少的独立性。

问答内容

2. 利用示例

public class ThreadLocalTest {
    private static String strLabel;
    private static ThreadLocal<String> threadLabel = new ThreadLocal<>();

    public static void main(String... args) {
        strLabel = "main";
        threadLabel.set("main");

        Thread thread = new Thread() {

            @Override
            public void run() {
                super.run();
                strLabel = "child";
                threadLabel.set("child");
            }

        };

        thread.start();
        try {
            // 保证线程执行完毕
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("strLabel = " + strLabel);
        System.out.println("threadLabel = " + threadLabel.get());
    }
}

运转结果:

strLabel = child
threadLabel = main

由运行结果好视,对于 ThreadLocal
类型的变量,在一个线程中设置值,不影响该以另外线程中之价值。也就是说
ThreadLocal 类型的变量的价值当每个线程中凡独自的。

1.

问:ThreadLocal了解也?您能够被自身说说他的主要用途吗?

答:

  • 从JAVA官方对ThreadLocal类似的验证定义(定义在示范代码中):ThreadLocal类用来供线程内部的片变量。这种变量在差不多线程环境下看(通过getset计访问)时亦可管各个线程的变量相对独立为其他线程内的变量。ThreadLocal实例通常来说还是private static类型的,用于关联线程和线程上下文。

  • 我们可以得知ThreadLocal的用意是:ThreadLocal的意是提供线程内的片段变量,不同之线程之间无会见相互干扰,这种变量在线程的生命周期内于作用,减少以及一个线程内大多单函数或机件之间有些公变量的传递的复杂度。

  • 上述可以概述为:ThreadLocal供线程内部的有变量,在本线程内随时随地可取,隔离其他线程。

演示代码:

/**
 * 该类提供了线程局部 (thread-local) 变量。 这些变量不同于它们的普通对应物,
 * 因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量
 * 它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段
 * 它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
 *
 * 例如,以下类生成对每个线程唯一的局部标识符。
 * 
 * 线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,
 * 在后续调用中不会更改。
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // 原子性整数,包含下一个分配的线程Thread ID 
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // 每一个线程对应的Thread ID
 *     private static final ThreadLocal<Integer> threadId =
 *         new ThreadLocal<Integer>() {
 *             @Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // 返回当前线程对应的唯一Thread ID, 必要时会进行分配
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * 每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的
 * 在线程消失之后,其线程局部实例的所有副本都会被垃圾回收,(除非存在对这些副本的其他引用)。
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */
public class ThreadLocal<T> {
·····
   /**
     * 自定义哈希码(仅在ThreadLocalMaps中有用)
     * 可用于降低hash冲突
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * 生成下一个哈希码hashCode. 生成操作是原子性的. 从0开始
     * 
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();


    /**
     * 表示了连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量 
     */
    private static final int HASH_INCREMENT = 0x61c88647;


    /**
     * 返回下一个哈希码hashCode
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
·····

}
  • 其中nextHashCode()计就是是一个原子类非停歇地去加上0x61c88647,这是一个颇特别之高频,叫斐波那契散列(Fibonacci
    Hashing),斐波那么契又发生一个名称叫黄金分割,也就是说将这累作为哈希值的增量将会如哈希表的分布更为均匀。

3. ThreadLocal 实现

ThreadLocal 是何许保证其价在逐个线程中凡是单独的也?下面分析下 ThreadLocal
的落实。

ThreadLocal 是构造函数只是一个简的无参构造函数,并且没有外实现。

2.

问:ThreadLocal兑现原理是什么,它是怎样就有变量不同之线程之间无会见相干扰的?

答:

  • 常备,如果我未错过看源代码的话,我猜ThreadLocal凡这样子设计之:每个ThreadLocal仿佛都创造一个Map,然后用线程的ID
    threadID作为Mapkey,要存储的一些变量作为Mapvalue,这样就会达标各个线程的价隔离的功能。这是无比简单易行的筹划方法,JDK最初期的ThreadLocal即是这么设计之。

  • 然而,JDK后面优化了设计方案,现时JDK8
    ThreadLocal的宏图是:每个Thread保障一个ThreadLocalMap哈希表,这个哈希表的keyThreadLocal实例本身,value才是确实使存储的价Object

  • 此规划与我们同样开始说的设计刚好相反,这样设计出如下几碰优势:

    1)
    这样设计后每个Map存储的Entry数码就会转换多少,因为事先的蕴藏数量由Thread的数量控制,现在凡由ThreadLocal的数控制。

    2)
    Thread销毁后,对应的ThreadLocalMap为会随之销毁,能减小内存的使用。

ThreadLocal引用关系图- 图片来自于《简书 –
对ThreadLocal实现原理的某些思想》

上述解释主要参考自:ThreadLocal和synchronized的区别?

3.1 set(T value) 方法

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(T value) 方法中,首先得当前线程,然后于获得到即线程的
ThreadLocalMap,如果 ThreadLocalMap 不也 null,则拿 value 保存到
ThreadLocalMap 中,并因而时 ThreadLocal 作为 key;否则创建一个
ThreadLocalMap 并被到当下线程,然后保留 value。

ThreadLocalMap 相当给一个 HashMap,是确实保存值的地方。

3.

问:您能够说说ThreadLocal常用操作的最底层实现原理为?如存储set(T value),获取get(),删除remove()等操作。

答:

  • 调用get()操作获取ThreadLocal饱受针对应当前线程存储的值时,进行了如下操作:

    1 )
    获取当前线程Thread靶,进而获取这线程对象吃保护的ThreadLocalMap对象。

    2 ) 判断当前之ThreadLocalMap是不是存在:

  • 如若是,则盖目前的ThreadLocal
    key,调用ThreadLocalMap中的getEntry计得到相应之贮存实体
    e。找到呼应的积存实体 e,获取存储实体 e 对应之
    value价,即为咱纪念要之当前线程对许者ThreadLocal的价,返回结果值。

  • 若是不存在,则证实这个线程没有保护的ThreadLocalMap对象,调用setInitialValue术进行初始化。返回setInitialValue初始化的值。

  • setInitialValue道的操作如下:

    1 ) 调用initialValue取得初始化的价值。

    2 )
    获取当前线程Thread靶,进而获取这线程对象吃保护的ThreadLocalMap对象。

    3 ) 判断当前之ThreadLocalMap是不是留存:

  • 苟在,则调用map.set安这个实体entry

  • 使未在,则调用createMap进行ThreadLocalMap靶的初始化,并将是实体entry当第一独价值存放到ThreadLocalMap中。

PS:关于ThreadLocalMap对应之系操作,放在下一个题材详细说明。

以身作则代码:

    /**
     * 返回当前线程对应的ThreadLocal的初始值
     * 此方法的第一次调用发生在,当线程通过{@link #get}方法访问此线程的ThreadLocal值时
     * 除非线程先调用了 {@link #set}方法,在这种情况下,
     * {@code initialValue} 才不会被这个线程调用。
     * 通常情况下,每个线程最多调用一次这个方法,
     * 但也可能再次调用,发生在调用{@link #remove}方法后,
     * 紧接着调用{@link #get}方法。
     *
     * <p>这个方法仅仅简单的返回null {@code null};
     * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
     * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
     * 通常, 可以通过匿名内部类的方式实现
     *
     * @return 当前ThreadLocal的初始值
     */
    protected T initialValue() {
        return null;
    }

    /**
     * 创建一个ThreadLocal
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

    /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     *
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 找到对应的存储实体 e 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        // 如果map不存在,则证明此线程没有维护的ThreadLocalMap对象
        // 调用setInitialValue进行初始化
        return setInitialValue();
    }

    /**
     * set的变样实现,用于初始化值initialValue,
     * 用于代替防止用户重写set()方法
     *
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

    /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  • 调用set(T value)操作设置ThreadLocal中针对相应前线程要存储的价值时,进行了如下操作:

    1 )
    获取当前线程Thread对象,进而获得这线程对象被保障的ThreadLocalMap对象。

    2 ) 判断当前底ThreadLocalMap是否存在:

  • 若是有,则调用map.set装是实体entry

  • 比方不存,则调用createMap进行ThreadLocalMap靶的初始化,并以是实体entry作为第一单价值存放到ThreadLocalMap中。

演示代码:

    /**
     * 设置当前线程对应的ThreadLocal的值
     * 大多数子类都不需要重写此方法,
     * 只需要重写 {@link #initialValue}方法代替设置当前线程对应的ThreadLocal的值
     *
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     *  
     */
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
            createMap(t, value);
    }

    /**
     * 为当前线程Thread 创建对应维护的ThreadLocalMap. 
     *
     * @param t the current thread 当前线程
     * @param firstValue 第一个要存放的ThreadLocal变量值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • 调用remove()操作删除ThreadLocal中对相应前线程已囤积的值经常,进行了之类操作:

    1 )
    获取当前线程Thread目标,进而获取之线程对象中保护的ThreadLocalMap对象。

    2 ) 判断时之ThreadLocalMap是不是有,
    如果存在,则调用map.remove,以当前ThreadLocalkey去除相应之实业entry

  • 示范代码:

    /**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     * 如果此ThreadLocal变量在当前线程中调用 {@linkplain #get read}方法
     * 则会通过调用{@link #initialValue}进行再次初始化,
     * 除非此值value是通过当前线程内置调用 {@linkplain #set set}设置的
     * 这可能会导致在当前线程中多次调用{@code initialValue}方法
     *
     * @since 1.5
     */
     public void remove() {
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在则调用map.remove
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

3.2 get() 方法

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

一样的,在 get() 方法中吗会见获到当前线程的 ThreadLocalMap,如果
ThreadLocalMap 不呢 null,则将获得 key 为目前 ThreadLocal 的值;否则调用
setInitialValue() 方法返回初开始值,并保存至新创办的 ThreadLocalMap 中。

4.

问:对ThreadLocal的常用操作实际是本着线程Thread中的ThreadLocalMap进展操作,核心是ThreadLocalMap以此哈希表,你能讨论ThreadLocalMap的内部底层实现啊?

答:

  • ThreadLocalMap的底部实现是一个定制的自定义HashMap哈希表,核心组成元素来:

    1 ) Entry[] table;:底层哈希表 table,
    必要常需展开扩容,底层哈希表 table.length 长度要是2的n次方。

    2 ) int size;:实际存储键值对素个数 entries

    3 ) int threshold;:下同样不好扩容时的阈值,阈值 threshold =
    底层哈希表table的长度
    len * 2 / 3。当size >= threshold时,遍历table并删除keynull的要素,如果除去后size >= threshold*3/4时,需要对table进行扩容(详情请查看set(ThreadLocal<?> key, Object value)措施求证)。

  • 其中Entry[] table;哈希表存储的骨干因素是EntryEntry包含:

    1 ) ThreadLocal<?> k;:当前囤积的ThreadLocal实例对象

    2 ) Object value;:当前 ThreadLocal 对应储存的值value

  • 需要注意的凡,此Entry继承了回老家引用
    WeakReference,所以当使ThreadLocalMap时,发现key == null,则意味着这个key ThreadLocal无在吃引用,需要以那个自ThreadLocalMap哈希表中移除。(弱引用相关题材解释请查看
    问答 5)

以身作则代码:

    /**
     * ThreadLocalMap 是一个定制的自定义 hashMap 哈希表,只适合用于维护
     * 线程对应ThreadLocal的值. 此类的方法没有在ThreadLocal 类外部暴露,
     * 此类是私有的,允许在 Thread 类中以字段的形式声明 ,     
     * 以助于处理存储量大,生命周期长的使用用途,
     * 此类定制的哈希表实体键值对使用弱引用WeakReferences 作为key, 
     * 但是, 一旦引用不在被使用,
     * 只有当哈希表中的空间被耗尽时,对应不再使用的键值对实体才会确保被 移除回收。
     */
    static class ThreadLocalMap {

        /**
         * 实体entries在此hash map中是继承弱引用 WeakReference, 
         * 使用ThreadLocal 作为 key 键.  请注意,当key为null(i.e. entry.get()
         * == null) 意味着此key不再被引用,此时实体entry 会从哈希表中删除。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** 当前 ThreadLocal 对应储存的值value. */
            Object value;

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

        /**
         * 初始容量大小 16 -- 必须是2的n次方.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 底层哈希表 table, 必要时需要进行扩容.
         * 底层哈希表 table.length 长度必须是2的n次方.
         */
        private Entry[] table;

        /**
         * 实际存储键值对元素个数 entries.
         */
        private int size = 0;

        /**
         * 下一次扩容时的阈值
         */
        private int threshold; // 默认为 0

        /**
         * 设置触发扩容时的阈值 threshold
         * 阈值 threshold = 底层哈希表table的长度 len * 2 / 3
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * 获取该位置i对应的下一个位置index
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * 获取该位置i对应的上一个位置index
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

    }
  • ThreadLocalMap的构造方法是缓加载的,也就是说,只有当线程需要仓储对应的ThreadLocal的值经常,才初始化创建同不行(仅初始化一不善)。初始化步骤如下:

    1) 初始化底层数组table的开端容量为 16。

    2)
    获取ThreadLocal中的threadLocalHashCode,通过threadLocalHashCode & (INITIAL_CAPACITY - 1),即ThreadLocal
    的 hash 值 threadLocalHashCode % 哈希表的尺寸 length
    的道计算该实体的蕴藏位置。

    3) 存储时的实业,key 为 : 当前ThreadLocal value:真正要存储的价

    4)设置当前事实上存储元素个数 size 为 1

    5)设置阈值setThreshold(INITIAL_CAPACITY),为初始化容量 16 的
    2/3。

演示代码:

        /**
         * 用于创建一个新的hash map包含 (firstKey, firstValue).
         * ThreadLocalMaps 构造方法是延迟加载的,所以我们只会在至少有一个
         * 实体entry存放时,才初始化创建一次(仅初始化一次)。
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 初始化 table 初始容量为 16
            table = new Entry[INITIAL_CAPACITY];
            // 计算当前entry的存储位置
            // 存储位置计算等价于:
            // ThreadLocal 的 hash 值 threadLocalHashCode  % 哈希表的长度 length
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 存储当前的实体,key 为 : 当前ThreadLocal  value:真正要存储的值
            table[i] = new Entry(firstKey, firstValue);
            // 设置当前实际存储元素个数 size 为 1
            size = 1;
            // 设置阈值,为初始化容量 16 的 2/3。
            setThreshold(INITIAL_CAPACITY);
        }
  • ThreadLocalget()操作实际是调用ThreadLocalMapgetEntry(ThreadLocal<?> key)措施,此措施迅速适用于博有一样有key的实体
    entry,否则,应该调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)办法取得,这样做是为了最深范围地增长直接命中的性能,该方法进行了之类操作:

    1 )
    计算而获取的entry的贮存位置,存储位置计算等价于:ThreadLocal
    hashthreadLocalHashCode % 哈希表的长短 length

    2 ) 根据计算的囤积位置,获取到对应之实体
    Entry。判断相应实体Entry是否留存 并且 key是否等:

  • 有对应实体Entry并且对应key相等,即同一ThreadLocal,返回对应的实体Entry

  • 非存对应实体Entry 或者
    key匪齐,则通过调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)计继续搜寻。

  • getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)计操作如下:

    1 )
    获取底层哈希表数组table,循环遍历对诺要找的实业Entry所干的职务。

    2 ) 获取当前遍历的entry
    key ThreadLocal,比较key是否相同,一致则赶回。

    3 ) 如果key不一致 并且 key
    null,则证实引用已经不设有,这是盖Entry继往开来的是WeakReference,这是物化引用带来的坑。调用expungeStaleEntry(int staleSlot)措施去过期的实体Entry(此道不单独解释,请查看示例代码,有详实注解说明)。

    4 ) key不一致 ,key否未为空,则遍历下一个职务,继续寻找。

    5 ) 遍历完毕,仍然找不顶则回null

演示代码:

        /**
         * 根据key 获取对应的实体 entry.  此方法快速适用于获取某一存在key的
         * 实体 entry,否则,应该调用getEntryAfterMiss方法获取,这样做是为
         * 了最大限制地提高直接命中的性能
         *
         * @param  key 当前thread local 对象
         * @return the entry 对应key的 实体entry, 如果不存在,则返回null
         */
        private Entry getEntry(ThreadLocal<?> key) {
            // 计算要获取的entry的存储位置
            // 存储位置计算等价于:
            // ThreadLocal 的 hash 值 threadLocalHashCode  % 哈希表
            的长度 length
            int i = key.threadLocalHashCode & (table.length - 1);
            // 获取到对应的实体 Entry 
            Entry e = table[i];
            // 存在对应实体并且对应key相等,即同一ThreadLocal
            if (e != null && e.get() == key)
                // 返回对应的实体Entry 
                return e;
            else
                // 不存在 或 key不一致,则通过调用getEntryAfterMiss继续查找
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * 当根据key找不到对应的实体entry 时,调用此方法。
         * 直接定位到对应的哈希表位置
         *
         * @param  key 当前thread local 对象
         * @param  i 此对象在哈希表 table中的存储位置 index
         * @param  e the entry 实体对象
         * @return the entry 对应key的 实体entry, 如果不存在,则返回null
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            // 循环遍历当前位置的所有实体entry
            while (e != null) {
                // 获取当前entry 的 key ThreadLocal
                ThreadLocal<?> k = e.get();
               // 比较key是否一致,一致则返回
                if (k == key)
                    return e;
                // 找到对应的entry ,但其key 为 null,则证明引用已经不存在
                // 这是因为Entry继承的是WeakReference,这是弱引用带来的坑
                if (k == null)
                    // 删除过期(stale)的entry
                    expungeStaleEntry(i);
                else
                    // key不一致 ,key也不为空,则遍历下一个位置,继续查找
                    i = nextIndex(i, len);
                // 获取下一个位置的实体 entry
                e = tab[i];
            }
            // 遍历完毕,找不到则返回null
            return null;
        }


        /**
         * 删除对应位置的过期实体,并删除此位置后对应相关联位置key = null的实体
         *
         * @param staleSlot 已知的key = null 的对应的位置索引
         * @return 对应过期实体位置索引的下一个key = null的位置
         * (所有的对应位置都会被检查)
         */
        private int expungeStaleEntry(int staleSlot) {
            // 获取对应的底层哈希表 table
            Entry[] tab = table;
            // 获取哈希表长度
            int len = tab.length;

            // 擦除这个位置上的脏数据
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // 直到我们找到 Entry e = null,才执行rehash操作
            // 就是遍历完该位置的所有关联位置的实体
            Entry e;
            int i;
            // 查找该位置对应所有关联位置的过期实体,进行擦除操作
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // 我们必须一直遍历直到最后
                        // 因为还可能存在多个过期的实体
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        /**
         * 删除所有过期的实体
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }
  • ThreadLocalset(T value)操作实际是调用ThreadLocalMapset(ThreadLocal<?> key, Object value)措施,该方式开展了之类操作:

    1 ) 获取相应之平底哈希表table,计算对应threalocal的储存位置。

    2 ) 循环遍历table针对该位置的实体,查找对应之threadLocal

    3 )
    获取当前位置的threadLocal,如果key threadLocal如出一辙,则证实找到呼应的threadLocal,将新值赋值给找到的当前实体Entryvalue中,结束。

    4 )
    如果手上位置的key threadLocal不一致,并且key threadLocalnull,则调用replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)方法(此方无独立解释,请查看示例代码,有详尽注解说明),替换该位置key == null
    的实体为当前而设置的实业,结束。

    5 )
    如果手上职的key threadLocal不一致,并且key threadLocal不为null,则开创新的实业,并存放到时职务
    i
    tab[i] = new Entry(key, value);,实际存储键值对素个数size + 1,由于弱引用带来了之问题,所以要调用cleanSomeSlots(int i, int n)法清除无用数据(此方法不单独解释,请查看示例代码,有详尽注解说明),才能够判断现在底size来没发生高达阀值threshhold,如果无使排除的数量,存储元素个数仍然
    大于 阈值
    则调用rehash主意进行扩容(此道无独立解释,请查看示例代码,有详尽注解说明)。

演示代码:

        /**
         * 设置对应ThreadLocal的值
         *
         * @param key 当前thread local 对象
         * @param value 要设置的值
         */
        private void set(ThreadLocal<?> key, Object value) {

            // 我们不会像get()方法那样使用快速设置的方式,
            // 因为通常很少使用set()方法去创建新的实体
            // 相对于替换一个已经存在的实体, 在这种情况下,
            // 快速设置方案会经常失败。

            // 获取对应的底层哈希表 table
            Entry[] tab = table;
            // 获取哈希表长度
            int len = tab.length;
            // 计算对应threalocal的存储位置
            int i = key.threadLocalHashCode & (len-1);

            // 循环遍历table对应该位置的实体,查找对应的threadLocal
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                // 获取当前位置的ThreadLocal
                ThreadLocal<?> k = e.get();
                // 如果key threadLocal一致,则证明找到对应的threadLocal
                if (k == key) {
                    // 赋予新值
                    e.value = value;
                    // 结束
                    return;
                }
                // 如果当前位置的key threadLocal为null
                if (k == null) {
                    // 替换该位置key == null 的实体为当前要设置的实体
                    replaceStaleEntry(key, value, i);
                    // 结束
                    return;
                }
            }
            // 当前位置的k != key  && k != null
            // 创建新的实体,并存放至当前位置i
            tab[i] = new Entry(key, value);
            // 实际存储键值对元素个数 + 1
            int sz = ++size;
            // 由于弱引用带来了这个问题,所以先要清除无用数据,才能判断现在的size有没有达到阀值threshhold
            // 如果没有要清除的数据,存储元素个数仍然 大于 阈值 则扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                // 扩容
                rehash();
        }

        /**
         * 当执行set操作时,获取对应的key threadLocal,并替换过期的实体
         * 将这个value值存储在对应key threadLocal的实体中,无论是否已经存在体
         * 对应的key threadLocal
         *
         * 有一个副作用, 此方法会删除该位置下和该位置nextIndex对应的所有过期的实体
         *
         * @param  key 当前thread local 对象
         * @param  value 当前thread local 对象对应存储的值
         * @param  staleSlot 第一次找到此过期的实体对应的位置索引index
         *         .
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            // 获取对应的底层哈希表 table
            Entry[] tab = table;
            // 获取哈希表长度
            int len = tab.length;
            Entry e;

            // 往前找,找到table中第一个过期的实体的下标
            // 清理整个table是为了避免因为垃圾回收带来的连续增长哈希的危险
            // 也就是说,哈希表没有清理干净,当GC到来的时候,后果很严重

            // 记录要清除的位置的起始首位置
            int slotToExpunge = staleSlot;
            // 从该位置开始,往前遍历查找第一个过期的实体的下标
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // 找到key一致的ThreadLocal或找到一个key为 null的
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // 如果我们找到了key,那么我们就需要把它跟新的过期数据交换来保持哈希表的顺序
                // 那么剩下的过期Entry呢,就可以交给expungeStaleEntry方法来擦除掉
                // 将新设置的实体放置在此过期的实体的位置上
                if (k == key) {
                    // 替换,将要设置的值放在此过期的实体中
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // 如果存在,则开始清除之前过期的实体
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // 在这里开始清除过期数据
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // / 如果我们没有在往后查找中找没有找到过期的实体,
                // 那么slotToExpunge就是第一个过期Entry的下标了
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 最后key仍没有找到,则将要设置的新实体放置
            // 在原过期的实体对应的位置上。
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // 如果该位置对应的其他关联位置存在过期实体,则清除
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }


        /**
         * 启发式的扫描查找一些过期的实体并清除,
         * 此方法会再添加新实体的时候被调用, 
         * 或者过期的元素被清除时也会被调用.
         * 如果实在没有过期数据,那么这个算法的时间复杂度就是O(log n)
         * 如果有过期数据,那么这个算法的时间复杂度就是O(n)
         * 
         * @param i 一个确定不是过期的实体的位置,从这个位置i开始扫描
         *
         * @param n 扫描控制: 有{@code log2(n)} 单元会被扫描,
         * 除非找到了过期的实体, 在这种情况下
         * 有{@code log2(table.length)-1} 的格外单元会被扫描.
         * 当调用插入时, 这个参数的值是存储实体的个数,
         * 但如果调用 replaceStaleEntry方法, 这个值是哈希表table的长度
         * (注意: 所有的这些都可能或多或少的影响n的权重
         * 但是这个版本简单,快速,而且似乎执行效率还可以)
         *
         * @return true 返回true,如果有任何过期的实体被删除。
         */
        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }


        /**
         * 哈希表扩容方法
         * 首先扫描整个哈希表table,删除过期的实体
         * 缩小哈希表table大小 或 扩大哈希表table大小,扩大的容量是加倍.
         */
        private void rehash() {
            // 删除所有过期的实体
            expungeStaleEntries();

            // 使用较低的阈值threshold加倍以避免滞后
            // 存储实体个数 大于等于 阈值的3/4则扩容
            if (size >= threshold - threshold / 4)
                resize();
        }

        /**
         * 扩容方法,以2倍的大小进行扩容
         * 扩容的思想跟HashMap很相似,都是把容量扩大两倍
         * 不同之处还是因为WeakReference带来的
         */
        private void resize() {
            // 记录旧的哈希表
            Entry[] oldTab = table;
            // 记录旧的哈希表长度
            int oldLen = oldTab.length;
            // 新的哈希表长度为旧的哈希表长度的2倍
            int newLen = oldLen * 2;
            // 创建新的哈希表
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            // 逐一遍历旧的哈希表table的每个实体,重新分配至新的哈希表中
            for (int j = 0; j < oldLen; ++j) {
                // 获取对应位置的实体
                Entry e = oldTab[j];
                // 如果实体不会null
                if (e != null) {
                    // 获取实体对应的ThreadLocal
                    ThreadLocal<?> k = e.get(); 
                    // 如果该ThreadLocal 为 null
                    if (k == null) {
                        // 则对应的值也要清除
                        // 就算是扩容,也不能忘了为擦除过期数据做准备
                        e.value = null; // Help the GC
                    } else {
                        // 如果不是过期实体,则根据新的长度重新计算存储位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                       // 将该实体存储在对应ThreadLocal的最后一个位置
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            // 重新分配位置完毕,则重新计算阈值Threshold
            setThreshold(newLen);
            // 记录实际存储元素个数
            size = count;
            // 将新的哈希表赋值至底层table
            table = newTab;
        }
  • ThreadLocalremove()操作实际是调用ThreadLocalMapremove(ThreadLocal<?> key)道,该办法开展了之类操作:

    1 ) 获取相应之根哈希表 table,计算对应threalocal的积存位置。

    2 ) 循环遍历table本着应该位置的实体,查找对应之threadLocal

    3 )
    获取当前岗位的threadLocal,如果key threadLocal同,则说明找到相应的threadLocal,执行删除操作,删除此职务的实体,结束。

示范代码:

        /**
         * 移除对应ThreadLocal的实体
         */
        private void remove(ThreadLocal<?> key) {
            // 获取对应的底层哈希表 table
            Entry[] tab = table;
            // 获取哈希表长度
            int len = tab.length;
            // 计算对应threalocal的存储位置
            int i = key.threadLocalHashCode & (len-1);
            // 循环遍历table对应该位置的实体,查找对应的threadLocal
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                // 如果key threadLocal一致,则证明找到对应的threadLocal
                if (e.get() == key) {
                    // 执行清除操作
                    e.clear();
                    // 清除此位置的实体
                    expungeStaleEntry(i);
                    // 结束
                    return;
                }
            }
        }

3.3 initialValue() 方法:

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
...

initialValue() 是 ThreadLocal 的开头值,默认返回
null,子类可以还写改措施,用于安装 ThreadLocal 的初始值。

5.

问:ThreadLocalMap遭之蕴藏实体Entry使用ThreadLocal作为key,但这个Entry大凡随着承弱引用WeakReference的,为什么要如此设计,使用了身故引用WeakReference会见招内存泄露问题为?

答:

  • 第一,回答这个题材之前,我急需解释一下什么是赛引用,什么是死引用。

俺们当常规状况下,普遍采用的凡赛引用:

A a = new A();

B b = new B();

a = null;b = null;时,一段时间后,JAVA垃圾回收机制GC会将 a 和 b
对诺所分配的内存空间给回收。

而是考虑这么平等栽状态:

C c = new C(b);
b = null;

当 b 被设置成null经常,那么是否代表这一段时间后GC工作可回收 b
所分配的内存空间呢?答案是否定的,因为纵 b 被安装成null,但 c
仍然保有对 b 的援,而且还是高引用,所以GC不会见回收 b
原先所分配的上空,既无克回收,又未可知运用,这虽导致了 内存泄露。

那如何处理呢?

得经过c = null;,也足以采用弱引用WeakReference w = new WeakReference(b);。因为使用了寿终正寝引用WeakReference,GC是可回收
b 原先所分配的空中的。

上述解释根本参考自:本着ThreadLocal实现原理的一点思想

  • 回到ThreadLocal的面达到,ThreadLocalMap使用ThreadLocal的身故引用作为key,如果一个ThreadLocal不曾外部强引用来引用它,那么网
    GC
    的下,这个ThreadLocal一定会为回收,这样一来,ThreadLocalMap惨遭即会见并发keynullEntry,就从未艺术看这些keynullEntryvalue,如果手上线程再缓缓未收以来,这些keynullEntryvalue不怕会一直存在一样长大引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
    永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的统筹受到早就考虑到这种场面,也添加了一部分备措施:在ThreadLocalget(),set(),remove()的时节都见面去掉线程ThreadLocalMap里所有keynullvalue

但是这些被动之预防措施并无可知确保非见面内存泄漏:

  • 使用staticThreadLocal,延长了ThreadLocal的生命周期,可能造成的内存泄漏(参考ThreadLocal
    内存泄露的实例分析)。

  • 分配使用了ThreadLocal以不再调用get(),set(),remove()计,那么就见面导致内存泄漏。

自打外表上看内存泄漏的发源在用了寿终正寝引用。网上的文章大都着重分析ThreadLocal使用了身故引用会招内存泄漏,但是别一个问题吧同值得沉思:为什么用弱引用而休是赛引用?

我们事先来看看官方文档的布道:

To help deal with very large and long-lived usages, 
the hash table entries use WeakReferences for keys.

以回生可怜及长日子的用,哈希表使用弱引用的 key

下面我们分开点儿种情形讨论:

  • key
    使用大引用:引用的ThreadLocal的目标为回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没手动删除,ThreadLocal非见面给回收,导致Entry内存泄漏。

  • key
    使用弱引用:引用的ThreadLocal的目标为回收了,由于ThreadLocalMap持有ThreadLocal的故引用,即使没有手动删除,ThreadLocal否会吃回收。value以生一样浅ThreadLocalMap调用get(),set(),remove()的时段会给消除。

  • 比简单种情形,我们得以窥见:由于ThreadLocalMap的生命周期跟Thread如出一辙长,如果都没手动删除相应key,都见面导致内存泄漏,但是使用弱引用得基本上一致层保障:弱引用ThreadLocal切莫见面内存泄漏,对应之value每当产同样涂鸦ThreadLocalMap调用get(),set(),remove()的时候会叫破。

因此,ThreadLocal内存泄漏的发源是:由于ThreadLocalMap的生命周期跟Thread平等长,如果无手动删除相应key便见面导致内存泄漏,而非是为弱引用。

汇总上面的分析,我们好了解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏也?

每次用完毕ThreadLocal,都调用它的remove()术,清除数据。

以使用线程池的情事下,没有应声清理ThreadLocal,不仅是内存泄漏的问题,更严重的是唯恐致业务逻辑出现问题。所以,使用ThreadLocal便跟加锁完要解锁一样,用完便清理。

上述解释主要参考自:深切剖析 ThreadLocal
内存泄漏问题

3.4 remove() 方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

ThreadLocal 还有一个 remove() 方法,用来移除当前 ThreadLocal
对应的价值。同样为是与过手上线程的 ThreadLocalMap 来移除相应的值。

6.

问:ThreadLocalsynchronized的区别?

答:ThreadLocalsynchronized重大字还用于拍卖多线程并发访问变量的问题,只是二者处理问题的角度和思路不同。

  1. ThreadLocal举凡一个Java类,通过对现阶段线程中之片段变量的操作来化解不同线程的变量访问的撞问题。所以,ThreadLocal提供了线程安全之共享对象机制,每个线程都有所其副本。

  2. Java中的synchronized大凡一个保留字,它凭借JVM的锁机制来促成临界区的函数或者变量的访中之原子性。在共机制面临,通过对象的锁机制保证同一时间只来一个线程访问变量。此时,被用作“锁机制”的变量时差不多只线程共享的。

  3. 齐机制(synchronized一言九鼎字)采用了为“时间更换空间”的点子,提供平等客变量,让不同的线程排队访问。而ThreadLocal动用了“以空间更换时间”的方,为各国一个线程都提供相同卖变量的副本,从而实现而做客使互不影响。

3.5 当前线程的 ThreadLocalMap

以 set,get,initialValue 和 remove
方法中还见面获到当下线程,然后通过时线程获取到 ThreadLocalMap,如果
ThreadLocalMap 为 null,则会创一个 ThreadLocalMap,并为到手上线程。

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

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...

足见见,每一个线程都见面有着有一个
ThreadLocalMap,用来维护线程本地的价值:

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}

当采取 ThreadLocal 类型变量进行连锁操作时,都见面由此时线程获取到
ThreadLocalMap 来完成操作。每个线程的 ThreadLocalMap
是属于线程自己的,ThreadLocalMap
中维护的值也是属于线程自己的。这就算保险了 ThreadLocal
类型的变量在每个线程中是独立的,在多线程环境下不会见相互影响。

7.

问:ThreadLocal每当今起什么用场景?

报经:总的来说ThreadLocal最主要是化解2种植档次的题目:

  • 缓解出现问题:使用ThreadLocal代替synchronized来管线程安全。同步机制使了“以日变空间”的措施,而ThreadLocal采用了“以空间更换时间”的方法。前者只提供平等客变量,让不同的线程排队访问,而后者也每一个线程都提供了同等客变量,因此可以做客使互不影响。

  • 解决数据存储问题:ThreadLocal否变量在每个线程中都创了一个副本,所以每个线程可以拜自己内部的副本变量,不同线程之间无见面相干扰。如一个Parameter目标的数额要在差不多只模块中采取,如果利用参数传递的方法,显然会增多模块之间的耦合性。此时咱们得以用ThreadLocal解决。

动用场景:

Spring使用ThreadLocal釜底抽薪线程安全问题

  • 咱俩明白在形似情形下,只有无状态的Bean才堪在多线程环境下共享,在Spring受,绝大部分Bean犹足以声明也singleton作用域。就是盖Spring对一些Bean(如RequestContextHolderTransactionSynchronizationManagerLocaleContextHolder当)中非线程安全状态下ThreadLocal进展处理,让其啊成线程安全的状态,因为来状态的Bean尽管可在多线程中共享了。

  • 一般的Web运细分为表现层、服务层和持久层三单层次,在不同之交汇中编对应的逻辑,下层通过接口向上层开放意义调用。在形似情形下,从接收请求到回响应所经的兼具程序调用都与属于一个线程ThreadLocal凡解决线程安全问题一个分外好之思绪,它通过为每个线程提供一个独门的变量副本解决了变量并发访问的冲问题。在博情形下,ThreadLocal比较直下synchronized协机制解决线程安全题材还简明,更方便,且结果程序有所又强之并发性。

示范代码:

public abstract class RequestContextHolder  {
····

    private static final boolean jsfPresent =
            ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());

    private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
            new NamedThreadLocal<RequestAttributes>("Request attributes");

    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
            new NamedInheritableThreadLocal<RequestAttributes>("Request context");

·····
}

4. ThreadLocalMap

总结

  1. ThreadLocal供线程内部的一对变量,在本线程内随时随地可取,隔离其他线程。

  2. ThreadLocal的筹划是:每个Thread保障一个ThreadLocalMap哈希表,这个哈希表的keyThreadLocal实例本身,value才是真正要存储的价Object

  3. ThreadLocal的常用操作实际是对线程Thread中的ThreadLocalMap进行操作。

  4. ThreadLocalMap的底部实现是一个定制的自定义HashMap哈希表,ThreadLocalMap的阈值threshold
    = 底层哈希表table的长度 len * 2 / 3,当实际存储元素个数size
    大于或等 阈值threshold3/4
    size >= threshold*3/4,则对根哈希表数组table进行扩容操作。

  5. ThreadLocalMap吃之哈希表Entry[] table积存的主导要素是Entry,存储的keyThreadLocal实例对象,valueThreadLocal
    对承诺储存的值value。需要注意的是,此Entry累了身故引用
    WeakReference,所以当利用ThreadLocalMap时,发现key == null,则象征这个key ThreadLocal免在受引用,需要以那于ThreadLocalMap哈希表中移除。

  6. ThreadLocalMap使用ThreadLocal的辞世引用作为key,如果一个ThreadLocal未曾外部强引用来引用它,那么网
    GC
    的早晚,这个ThreadLocal早晚会给回收。所以,在ThreadLocalget(),set(),remove()的时光还见面去掉线程ThreadLocalMap里所有keynullvalue。如果我们无主动调用上述操作,则会促成内存泄露。

  7. 为了安全地利用ThreadLocal,必须使像每次用完锁就解锁一样,在每次用完毕ThreadLocal继都使调用remove()来清理无用的Entry。这当操作以用线程池时尤为重大。

  8. ThreadLocalsynchronized的别:同步机制(synchronized要害字)采用了因“时间换空间”的法门,提供相同卖变量,让不同之线程排队访问。而ThreadLocal采用了“以空间更换时间”的道,为各级一个线程都提供平等份变量的副本,从而实现以做客使互不影响。

  9. ThreadLocal最主要是化解2种类型的问题:A.
    解决出现问题:使用ThreadLocal代表同步机制解决出现问题。B.
    解决数据存储问题:如一个Parameter目标的数额要以差不多只模块中行使,如果使用参数传递的章程,显然会增加模块之间的耦合性。此时咱们好以ThreadLocal解决。

4.1 构造方法

ThreadLocal 中时线程的 ThreadLocalMap 为 null 时会见使用 ThreadLocalMap
的构造方法新建一个 ThreadLocalMap:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

构造方法中会新建一个数组,并拿拿第一差用保留的键值存储到一个数组中,完成有初始化工作。

参照文章

深入浅出ThreadLocal
ThreadLocal和synchronized的区别?
深入剖析ThreadLocal
ThreadLocal内部机制
聊一聊Spring中的线程安全性
对ThreadLocal实现原理的某些考虑
深刻剖析 ThreadLocal
内存泄漏问题
学Spring必学的Java基础知识(6)—-ThreadLocal
ThreadLocal设计模式
ThreadLocal案例解析
Spring单例模式以及线程安全ThreadLocal

4.2 存储结构

ThreadLocalMap
内部维护了一个哈希表(数组)来囤积数据,并且定义了加载因子:

// 初始容量,必须是 2 的幂
private static final int INITIAL_CAPACITY = 16;

// 存储数据的哈希表
private Entry[] table;

// table 中已存储的条目数
private int size = 0;

// 表示一个阈值,当 table 中存储的对象达到该值时就会扩容
private int threshold;

// 设置 threshold 的值
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

table 是一个 Entry 类型的频繁组,Entry 是 ThreadLocalMap 的一个中间类。

4.3 存储对象 Entry

Entry 用于保存一个键值对,其中 key 以弱引用的点子保留:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

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

4.4 保存键值对

调用 set(ThreadLocal key, Object value) 方法以数据必威保存到哈希表中:

private void set(ThreadLocal key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    // 计算要存储的索引位置
    int i = key.threadLocalHashCode & (len-1);

    // 循环判断要存放的索引位置是否已经存在 Entry,若存在,进入循环体
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();

        // 若索引位置的 Entry 的 key 和要保存的 key 相等,则更新该 Entry 的值
        if (k == key) {
            e.value = value;
            return;
        }

        // 若索引位置的 Entry 的 key 为 null(key 已经被回收了),表示该位置的 Entry 已经无效,用要保存的键值替换该位置上的 Entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 要存放的索引位置没有 Entry,将当前键值作为一个 Entry 保存在该位置
    tab[i] = new Entry(key, value);
    // 增加 table 存储的条目数
    int sz = ++size;
    // 清除一些无效的条目并判断 table 中的条目数是否已经超出阈值
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash(); // 调整 table 的容量,并重新摆放 table 中的 Entry
}

第一使用 key(当前 ThreadLocal)的 threadLocalHashCode
来计算而存储的目录位置 i。threadLocalHashCode 的值由 ThreadLocal
类管理,每创建一个 ThreadLocal 对象还见面自动生成一个遥相呼应的
threadLocalHashCode 值,其落实如下:

// ThreadLocal 对象的 HashCode
private final int threadLocalHashCode = nextHashCode();

// 使用 AtomicInteger 保证多线程环境下的同步
private static AtomicInteger nextHashCode =
    new AtomicInteger();

// 每次创建 ThreadLocal 对象是 HashCode 的增量
private static final int HASH_INCREMENT = 0x61c88647;

// 计算 ThreadLocal 对象的 HashCode
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

当保留数据时,如果索引位置发生 Entry,且该 Entry 的 key 为
null,那么即便会见履行清除无效 Entry 的操作,因为 Entry 的 key
使用的是去世引用的方,key 如果吃回收(即 key 为
null),这时便无法再拜访到 key 对应的 value,需要将这么的无效 Entry
清除掉来腾出空间。

当调动 table 容量时,也会见事先破无效对象,然后重新冲需要扩容。

private void rehash() {
    // 先清除无效 Entry
    expungeStaleEntries();
    // 判断当前 table 中的条目数是否超出了阈值的 3/4
    if (size >= threshold - threshold / 4)
        resize();
}

消除无用对象同扩容的法子这里虽不再进行说明了。

4.5 获取 Entry 对象

取值是直接沾到 Entry 对象,使用 getEntry(ThreadLocal key) 方法:

private Entry getEntry(ThreadLocal key) {
    // 使用指定的 key 的 HashCode 计算索引位置
    int i = key.threadLocalHashCode & (table.length - 1);
    // 获取当前位置的 Entry
    Entry e = table[i];
    // 如果 Entry 不为 null 且 Entry 的 key 和 指定的 key 相等,则返回该 Entry
    // 否则调用 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

因可能是哈希冲突,key 对应的 Entry 的囤积位置或不以通过 key
计算起之目录位置上,也就是说索引位置及之 Entry 不肯定是 key 对应的
Entry。所以需要调用 getEntryAfterMiss(ThreadLocal key, int i, Entry e)
方法得到。

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 索引位置上的 Entry 不为 null 进入循环,为 null 则返回 null
    while (e != null) {
        ThreadLocal k = e.get();
        // 如果 Entry 的 key 和指定的 key 相等,则返回该 Entry
        if (k == key)
            return e;
        // 如果 Entry 的 key 为 null (key 已经被回收了),清除无效的 Entry
        // 否则获取下一个位置的 Entry,循环判断
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

4.6 移除指定的 Entry

private void remove(ThreadLocal key) {
    Entry[] tab = table;
    int len = tab.length;
    // 使用指定的 key 的 HashCode 计算索引位置
    int i = key.threadLocalHashCode & (len-1);
    // 循环判断索引位置的 Entry 是否为 null
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 若 Entry 的 key 和指定的 key 相等,执行删除操作
        if (e.get() == key) {
            // 清除 Entry 的 key 的引用
            e.clear();
            // 清除无效的 Entry
            expungeStaleEntry(i);
            return;
        }
    }
}

4.7 内存泄漏

当 ThreadLocalMap 的 set(),get() 和 remove() 方法被,都产生清除无效 Entry
的操作,这样做是为降低内存泄漏发生的也许。

Entry 中之 key
使用了寿终正寝引用的道,这样做是为了降低内存泄漏发生的几率,但非可知完全避免内存泄漏。

随即句话的意思好象是矛盾的,下面来分析一下。

如果 Entry 的 key 没有使用弱引用的法门,而是采取了大引用:由于
ThreadLocalMap 的生命周期和眼前线程一样长,那么当引用 ThreadLocal
的靶子吃回收后,由于 ThreadLocalMap 还装有 ThreadLocal 和呼应 value
的高引用,ThreadLocal 和相应的 value
是免会见于回收的,这虽招了内存泄漏。所以 Entry 以弱引用的不二法门避免了
ThreadLocal 没有被回收而造成的内存泄漏,但是此时 value
仍然是力不从心回收的,依然会招致内存泄漏。

ThreadLocalMap 已经考虑到这种景象,并且产生一对防止章程:在调用
ThreadLocal 的 get(),set() 和 remove() 的当儿还见面败当前线程
ThreadLocalMap 中有着 key 为 null 的
value。这样可降内存泄漏发生的票房价值。所以我们于使 ThreadLocal
的上,每次用完 ThreadLocal 都调用 remove()
方法,清除数据,防止内存泄漏。

相关文章

Leave a Comment.