Java并发编程——ThreadLocal

1.ThreadLocal

1.1 ThreadLocal简介

ThreadLocal 通过为每个使用可变状态的变量的线程维护一份该变量的独立的副本,即每个线程都会维护一份属于自己的这个共享变量的副本,彼此之间的操作互相独立,并不影响。

想像一下,多个人(多线程)有一个篮子(共享变量),多个人同时往篮子中加水果很容里造成篮子水果数量的不正确性。所以ThreadLocal的作用相当于为每个人买了一个单独的篮子,这样每个人操作属于自己的篮子时就不会出错了———线程封闭

1.2 ThreadLocal 源码分析

Java并发编程——ThreadLocal
上图展示了Thread,ThreadLocal 和 ThreadLocalMap 三者的关系

ThreadLocal ——线程变量:clock1:

ThreadLocal 从字面上理解为 线程变量,即与线程有关。所以首先需要关注一个点,即每个线程都会维护两个 ThreadLocal.ThreadLocalMap 类型的变量 : threadLocalsinheritableThreadLocals。

    //这两行代码均位于Thread类下,即属于线程的!
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocal.ThreadLocalMap——静态内部类?:clock2:

到这里不难发现——ThreadLocal.ThreadLocalMap这种写法意味着 ThreadLocal 中有一个静态内部类——ThreadLocalMap。那我们来看一下这个静态内部类的定义

Java并发编程——ThreadLocal

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> { 
        //entry竟然继承自弱引用???有意思
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;


         //毕竟叫ThreadLocalMap,应该和HashMap有差不多的数据结构吧?
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

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

        /**
         * 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 = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
    .....
}

通过阅读ThreadLocalMap的源码我们能获得以下信息:

  1. 其数据结构与HashMap类似,但其Entry继承自弱引用类——WeakReference< ThreadLocal<?> >,说明key值是弱引用,如果处理不当,会造成内存泄漏
  2. ThreadLocalMap与很多容器例如list和map类似,都是懒加载,即当第一次真正的往数组中添加元素的时候才会去执行构造函数来进行初始化
  3. ThreadLocalMap 也有相应的rehash和resize方法,但是由于其没有链表这种数据结构,故其发生hash冲突的时候有其他方法处理

ThreadLocalMap的哈希算法 :clock3:

既然是Map结构,那么ThreadLocalMap当然也要实现自己的hash算法来决定每一个entry的存放位置

int i = key.threadLocalHashCode & (len-1);

可以发现,这里取决于key(即ThreadLocal的对象)的哈希值——threadLocalHashCode

    private final int threadLocalHashCode = nextHashCode();
    //保存全局静态工厂方法生成的属于自己的唯一的哈希值

    private static AtomicInteger nextHashCode =new AtomicInteger(); 
    //全局静态变量,初始化一次,因为所有线程共享,所以需要保证线程安全,故设置为AtomicInteger!

    private static final int HASH_INCREMENT = 0x61c88647; 
    //黄金分割数

    private static int nextHashCode() {
        //所有线程共用一个静态工厂,专门生成哈希值
        return nextHashCode.getAndAdd(HASH_INCREMENT); 
    }

读到这里我们发现,threadLocalHashCode是通过一个全局的静态工厂方法 nextHashCode 生成一个属于自己的哈希值,每当有一个ThreadLocal对象,其就增加0x61c88647!
这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字时 hash 分布非常均匀。尽管有时也可能会发生哈希冲突:boom::boom::boom:

测试黄金分割数

public class Thread_Local {
    private static final int HASH_INCREMENT = 0x61c88647;
    public static void main(String[] args){
        int hash = 0;
        for(int i = 0; i < 16;i++){
            hash+=HASH_INCREMENT;
            System.out.println("当前元素的位置:" + (hash & 15));
        }
    }
}

输出结果,可以看到每个元素的分布非常均匀

当前元素的位置:7
当前元素的位置:14
当前元素的位置:5
当前元素的位置:12
当前元素的位置:3
当前元素的位置:10
当前元素的位置:1
当前元素的位置:8
当前元素的位置:15
当前元素的位置:6
当前元素的位置:13
当前元素的位置:4
当前元素的位置:11
当前元素的位置:2
当前元素的位置:9
当前元素的位置:0

ThreadLocalMap的哈希冲突 :clock4:

既然ThreadLocalMap也是根据哈希算法来存放元素,当然其就会有哈希冲突!

注明: 下面所有示例图中,绿色块Entry代表正常数据灰色块代表Entrykey值为null已被垃圾回收白色块表示Entrynull

虽然ThreadLocalMap中使用了黄金分隔数来作为hash计算因子,大大减少了Hash冲突的概率,但是仍然会存在冲突。

HashMap中解决冲突的方法是在数组上构造一个链表结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树。而ThreadLocalMap中并没有链表结构,所以这里不能适用HashMap解决冲突的方式了。

整体上来看,ThreadLocalMap 采用开放地址法解决哈希冲突。
如上图所示,如果我们插入一个value=27的数据,通过hash计算后应该落入第4个槽位中,而槽位4已经有了Entry数据。此时就会线性向后查找,一直找到Entrynull的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了Entry不为nullkey值相等的情况,还有Entry中的key值为null的情况等等都会有不同的处理。

这里还画了一个Entry中的keynull的数据(Entry=2的灰色块数据),因为key值是弱引用类型,所以会有这种数据存在。在set过程中,如果遇到了key过期的Entry数据,实际上是会进行一轮探测式清理操作的。

ThreadLocalMap的核心方法——set( ) :clock5:

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) { //旧的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的set方法分为以下几种情况:

  1. 通过hash计算后的槽位对应的Entry数据为空,不进入for循环,直接生成添加新Entry即可:
    Java并发编程——ThreadLocal

  2. 槽位数据不为空,key与当前一致,直接更新返回:
    Java并发编程——ThreadLocal

  3. 槽位数据不为空且key不一致,即发生哈希冲突,采用开发地址法,向后探测,直到碰到相同的key值更新或者新的槽位添加(此过程没有过期key):
    Java并发编程——ThreadLocal

  4. 向后探测的过程中,出现过期的key(即下图灰色的entry),启动探测式清理,执行相应添加逻辑:
    Java并发编程——ThreadLocal
    散列数组下标为7位置对应的Entry数据keynull,表明此数据key值已经被垃圾回收掉了,此时就会执行replaceStaleEntry()方法,该方法含义是替换过期数据的逻辑,以index=7位起点开始遍历,进行探测式数据清理工作。以下为探测式数据清理的源码:

    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                         int staleSlot) {
              Entry[] tab = table;
              int len = tab.length;
              Entry e;
    
              // Back up to check for prior stale entry in current run.
              // We clean out whole runs at a time to avoid continual
              // incremental rehashing due to garbage collector freeing
              // up refs in bunches (i.e., whenever the collector runs).
              int slotToExpunge = staleSlot;
              for (int i = prevIndex(staleSlot, len);
                   (e = tab[i]) != null;
                   i = prevIndex(i, len))
                  if (e.get() == null)
                      slotToExpunge = i;
    
              // Find either the key or trailing null slot of run, whichever
              // occurs first
              for (int i = nextIndex(staleSlot, len);
                   (e = tab[i]) != null;
                   i = nextIndex(i, len)) {
                  ThreadLocal<?> k = e.get();
    
                  // If we find key, then we need to swap it
                  // with the stale entry to maintain hash table order.
                  // The newly stale slot, or any other stale slot
                  // encountered above it, can then be sent to expungeStaleEntry
                  // to remove or rehash all of the other entries in run.
                  if (k == key) {
                      e.value = value;
    
                      tab[i] = tab[staleSlot];
                      tab[staleSlot] = e;
    
                      // Start expunge at preceding stale entry if it exists
                      if (slotToExpunge == staleSlot)
                          slotToExpunge = i;
                      cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                      return;
                  }
    
                  // If we didn't find stale entry on backward scan, the
                  // first stale entry seen while scanning for key is the
                  // first still present in the run.
                  if (k == null && slotToExpunge == staleSlot)
                      slotToExpunge = i;
              }
    
              // If key not found, put new entry in stale slot
              tab[staleSlot].value = null;
              tab[staleSlot] = new Entry(key, value);
    
              // If there are any other stale entries in run, expunge them
              if (slotToExpunge != staleSlot)
                  cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
          }

1.3 ThreadLocal 使用注意事项

ThreadLocalMap中的key为弱引用类型,当其key无外部强引用时,会由垃圾收集器回收,进而造成key 为null,value还存在的情况,引发内存泄漏!解决方案可以对于已经不再使用的 ThreadLocal 变量应及时做remove处理!

测试代码 1

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        Thread t = new Thread(() -> test(123, true));
        t.start();

    }

    private static void test(Integer s,boolean isGC)  {
        try {
            ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>(); 
            //此处由于ThreadLocal还有一个强引用——threadLocal1,故垃圾回收器不可能对其回收
            threadLocal1.set(s);
            if (isGC) {
                System.gc();
                System.out.println("--gc后--");
            }
            Thread t = Thread.currentThread();
            Class<? extends Thread> clz = t.getClass();
            Field field = clz.getDeclaredField("threadLocals");
            field.setAccessible(true);
            Object ThreadLocalMap = field.get(t);
            Class<?> tlmClass = ThreadLocalMap.getClass();
            Field tableField = tlmClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object[] arr = (Object[]) tableField.get(ThreadLocalMap);
            for (Object o : arr) {
                if (o != null) {
                    Class<?> entryClass = o.getClass();
                    Field valueField = entryClass.getDeclaredField("value");
                    //获取value字段
                    Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                    //获取引用
                    valueField.setAccessible(true);
                    referenceField.setAccessible(true);

                    System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
--gc后--
弱引用key:java.lang.ThreadLocal@39054944,:123
弱引用key:java.lang.ThreadLocal@c88240f,:java.lang.ref.SoftReference@654839fd

输出结果第一行可以理解,毕竟还有threadLocal1这个强引用在,第二行输出非常令我迷惑,通过debug查看当前线程的threadlocals竟然发现其table的容量为3!如下图
Java并发编程——ThreadLocal
查看数组各元素如下:

Java并发编程——ThreadLocal

  1. 下标为 5 的数据的value是一个Object类型的数组,其内有一个UTF_8.Decoder类型的解码器
    Java并发编程——ThreadLocal
  1. 下标为 7 的数据的value是一个软引用,指向一个时间戳
    Java并发编程——ThreadLocal

  2. 下标为 10 的数据的value就是真正set进去的值

综上:我只新增了一个ThreadLocal< String >类型的变量,为何莫名其妙多出现两个???

测试代码 2

将ThreadLocal实例的强引用置空,观察输出结果

 ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
            threadLocal1.set(s);
            if (isGC) {
                threadLocal1 = null;
                System.gc();
                System.out.println("--gc后--");
            }
--gc后--
弱引用key:null,:123
弱引用key:java.lang.ThreadLocal@477a074c,:java.lang.ref.SoftReference@40b0b466

此处很明显,当前threadLocal并没有被完全清理,造成内存泄漏!

测试代码 3

将ThreadLocal采用安全的remove方法,观察输出结果

ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
            threadLocal1.set(s);

            if (isGC) {
                threadLocal1.remove();
                System.gc();
            }
--gc后--

Process finished with exit code 0

正常!

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!