ThreadLocal 实现

2021-05-15  本文已影响0人  lj72808up

一. ThreadLocal 如何实现和每个 Thread 绑定, 从而避免线程安全问题

  1. ThreadLocal 的类结构
    ThreadLocal 有静态内部类 ThreadLocalMap, ThreadLocalMap 有静态内部类 Entry. Entry 是键值对, 存储<ThreadLocal, Object>. 就是说每个 ThreadLocal 对象对应一个 object 的value.

  2. 每个 thread 对象都有唯一的 threadLocalMap 属性, 而 threadLocalMap 对象呗限制在一个线程中. 而 threadLocalMap 内部的Entry 又能存储多个 threadLocal 对象, 从而让 threadLocal 对象是线程安全的

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
  3. 为什么说 ThreadLocal 只能存储一个值? (Entry的关系)

    // 泛型
    public class ThreadLocal<T> {
    
        // 静态内部类 ThreadLocalMap 
        static class ThreadLocalMap {
            // ThreadLocal 静态内部类 Entry. 
            // Entry 的 key 是 ThreadLocal, value 是 ThreadLocal 中的值
            // 也因为 Entry 的 key 是 ThreadLocal, 所以其 value 只能有一个. 即 ThreadLocal 只能存储单个值
            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    
            // 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);
            }
        }
    }
    
  4. ThreadLocalMap 底层实际是一个 hash 表, 用 ThreadLocal 的 hashcode 值决定在 hash 表中的位置. 那如何让线程内, 每次 new ThreadLocal() 获得的对象其 hashCode 不一样呢?
    因为有一个类的全局静态属性 AtomicInteger 的 nextHashCode ,每次实例化对象时修改该值

    public class ThreadLocal<T> {
        // 实例化ThreadLocal 对象时, 使用 threadLocalHashCode 唯一标识一个 ThreadLocal 对象
        private final int threadLocalHashCode = nextHashCode();
        // 类级别属性, 每个 ThreadLocal 对象共享 nextHashCode 属性
        private static AtomicInteger nextHashCode = new AtomicInteger();
        // 常亮
        private static final int HASH_INCREMENT = 0x61c88647;
        private static int nextHashCode() {
              return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
    }
    
  5. 为什么 ThreadLocal 有内存泄漏问题
    因为每个 thread 对应唯一的 threadLocalMap 对象, 但 ThreadLocalMap.Entry 键值对内部可以存储多个 ThreadLocal key, 且这个 key 是 WeakReference. 如果代码中没有对 ThreadLocal 的强引用, 一旦 gc, 该 ThreadLocal 就会被置空, ThreadLocalMap.Entry 原来存储该 ThreadLocal 的 key 就会被置为 null, 从而客户端再也无法访问 key=null 的键值对对应的 value, 导致 value 泄露, 再也无法 gc.
    其实 jdk 内部在 threadLocal 对象的 get() 或 set(). remove() 方法内有对 ThreadLocalMap.Entry 中 k=null 键值对的清除操作. 只是该操作必须让客户端触发 get() 或 set(). remove() 方法. 所以 jdk 鼓励在 threadLocal 对象不再使用时, 用 threadLocal.remove() 方法清除. (不能直接置为 null , 这和软引用 gc 导致内存泄露的做法一样).

    static class 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);
        }
    
    
        // 获取键值对
        private Entry getEntry(ThreadLocal<?> key) {
            // 根据 ThreadLocal 对象的 hashcode 计算出当时 set 的数组位置
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                // 当 direct hash slot 中没有 ThreadLocal 对象时, 向下一个 slot 查找
                return getEntryAfterMiss(key, i, e);
        }
    
    
        // 向下一个 slot 查找 ThradLocal 对象
        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;
                    // 遇到 key 为 null 的, 就是已经被 jvm 清除的 ThreadLocal 弱引用 
                    // (只要没有强引用,一旦 gc , hreadLocal 对象就会被置空)
                    if (k == null)
                        // 该方法清除 key 为 null 的slot, 会缩减内部 hash 表的长度. 具体算法略
                        expungeStaleEntry(i);
                    else
                        i = nextIndex(i, len);
                    e = tab[i];
                }
                return null;
            }
    }
    
上一篇下一篇

猜你喜欢

热点阅读