Java并发工具类 - ThreadLocal

  • 1. ThreadLocal概念
    • 1.1 设计
  • 2. ThreadLocal常用方法
    • 2.1 set方法
    • 2.2 get方法
    • 2.3 remove方法
  • 3. ThreadLocalMap
    • 3.1 内部类Entry:
    • 3.2 内存泄漏问题
      • 原因
      • 解决

1. ThreadLocal概念

ThreadLocal用于线程间的数据隔离。应用场合,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

ThreadLocal是一个本地线程副本变量工具类。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

1.1 设计

JDK8 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的key是ThreadLocal实例本身,value才是真正要存储的值Object。

ThreadLocalMap是ThreadLocal内部类,由ThreadLocal创建,Thread有ThreadLocal.ThreadLocalMap类型的属性。

一个线程只维护一个ThreadLocalMap哈希表,其key是ThreadLocal实例,所以如果ThreadLocal是全局变量,则key只有一个,所以对应的Map的size为1(这种用法应该较常见)。如果有局部ThreadLocal变量,则ThreadLocalMap的元素可能有多个。

2. ThreadLocal常用方法

2.1 set方法

步骤:

  1. 获取当前线程的ThreadLocalMap对象threadLocals
  2. map非空,则重新将ThreadLocal和新的value副本放入到map中。调用的是ThreadLocalMap的set方法
  3. map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。

关键在于上面的第二步,ThreadLocalMap的set(ThreadLocal key, Object value)方法!

    // ThreadLocal.java
    public void set(T value) {
        // 返回当前执行的线程
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // key是ThreadLocal对象
            map.set(this, value);
        else
            createMap(t, value);
    }


    // getMap方法
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    // 返回的map是Thread类中定义的
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */

    //  Thread.java,每个Thread内部维护着一个ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // 如果返回的map不为空,则以当前ThreadLocal对象为key,set进map中
    // 如果返回的map为空,则创建一个,如下所示
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    // 初始化ThreadLocalMap。ThreadLocalMap的构造方法是延迟加载的,也就是说,只有当线程需要存储对应的ThreadLocal的值时,才初始化创建一次。
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];//初始容量16
        // ThreadLocal 的 hash 值 threadLocalHashCode与容量 进行&运算(2个数字进行&时,运算后的值小于2数之间小的那个)
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;//size为1
        setThreshold(INITIAL_CAPACITY);//阈值 = 16 * 2 / 3;
    }

ThreadLocalMap的set方法:

       private void set(ThreadLocal<?> key, Object value) {
            // ThreadLocalMap的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)]) {//每次+1

                // 获取当前位置的ThreadLocal
                ThreadLocal<?> k = e.get();
                // 如果key threadLocal一致,则证明找到对应的threadLocal,直接设置
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 如果当前位置的key threadLocal为null,替换过期的
                // key 为 null,则证明引用已经不存在;这是因为Entry继承的是WeakReference
                if (k == null) {
                    // 删除过期的entry
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // 当前位置的k != key  && k != null,创建新的实体,并存放至当前位置i
            tab[i] = new Entry(key, value);
            // 实际存储键值对元素个数 + 1,entries in the table.
            int sz = ++size;

            // 清除无用数据&扩容逻辑
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

2.2 get方法

  1. 获取当前线程的ThreadLocalMap对象threadLocals
  2. 从map中获取线程存储的K-V Entry节点。
  3. 从Entry节点获取存储的Value副本值,非空则返回;为空则返回null
  4. map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。

主要操作为第二步,用ThreadLocalMap的getEntry(ThreadLocal key)方法获取某一存在key的实体 entry

    public T get() {
        Thread t = Thread.currentThread();
        // getMap和set方法中一致
        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;
            }
        }
        // 设置初始值为null
        return setInitialValue();
    }

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

ThreadLocalMap的getEntry方法:

        private Entry getEntry(ThreadLocal<?> key) {
            // 要获取的entry的存储位置
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // 存在对应实体Entry并且对应key相等,即同一ThreadLocal
            if (e != null && e.get() == key)
                return e;
            else
            // 不存在对应实体Entry 或者 key不相等,则继续查找...
                return getEntryAfterMiss(key, i, e);
        }

2.3 remove方法

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

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

3. ThreadLocalMap

ThreadLocalMap的底层实现是一个定制的自定义HashMap哈希表。

3.1 内部类Entry:

    // 没有实现Map接口,用独立的方式实现了Map的功能
    static class ThreadLocalMap {
        // 用Entry来保存K-V结构数据
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            // Entry中key只能是ThreadLocal对象,Entry的构造方法已经指定
            Entry(ThreadLocal<?> k, Object v) {
                // 调用public WeakReference(T referent),弱引用
                super(k);
                value = v;
            }
        }

Entry继承自WeakReference弱引用,生命周期只能存活到下次GC前。但只有Key是弱引用类型的,Value并非弱引用。

ThreadLocalMap的成员变量:

        // 初始容量
        private static final int INITIAL_CAPACITY = 16;

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

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

        // 下一次扩容时的阈值
        private int threshold; // Default to 0

Entry[] table;哈希表存储的核心元素是Entry,Entry包含:

  1. ThreadLocal k;:当前存储的ThreadLocal实例对象

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

冲突的解决:

ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

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

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

3.2 内存泄漏问题

原因

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry(key=null) -> value, 永远无法回收,造成内存泄漏。

解决

官方防护:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

具体实践中,每次使用完ThreadLocal,都调用它的remove()方法,清除数据。养成习惯就像加锁完要解锁一样,用完就清理。


参考:

ThreadLocal-面试必问深度解析

JAVA并发-自问自答学ThreadLocal

你可能感兴趣的:(Java并发,Java并发编程)