zoukankan      html  css  js  c++  java
  • java 并发(七)--- ThreadLocal

         文章部分图片来自参考资料

    问题 :

    • ThreadLocal 底层原理
    • ThreadLocal 需要注意什么问题,造成问题的原因是什么,防护措施是什么

    ThreadLocal 概述    

             ThreadLocal 线程本地变量 ,是一个工具,可以让多个线程保持一个变量的副本,那么每个线程可以访问自己内部的副本变量。

            ThrealLocal

               ThreadLocal 结构图里面看到有两个内部类,一个 SuppliedThreadLocal , 一个ThreadLocalMap 。下面用一张图来说明线程使用的示意图。可以看到每个Thread 有个 ThreadLocalMap ,然后里面由hash值分列的的数组 Entry[] 。Entry 数据结构就是图中淡绿色框内所示。所以 ThreadLocal 里面放的 value 应该是放在Thread里面的。

    threadlocal

     

    ThreadLocal  源码分析

             ThreadLocal 下文简称 TL, TL最常见的方法就是 get 和 set 了。

      1 public void set(T value) {
      2         Thread t = Thread.currentThread();
      3         ThreadLocalMap map = getMap(t);
      4         if (map != null)
      5             map.set(this, value);
      6         else
      7             createMap(t, value);
      8 }
     
      1     public T get() {
      2         Thread t = Thread.currentThread();
      3         ThreadLocalMap map = getMap(t);
      4         if (map != null) {
      5             ThreadLocalMap.Entry e = map.getEntry(this);
      6             if (e != null) {
      7                 @SuppressWarnings("unchecked")
      8                 T result = (T)e.value;
      9                 return result;
     10             }
     11         }
     12         return setInitialValue();
     13     }
     
      1     ThreadLocalMap getMap(Thread t) {
      2         return t.threadLocals;
      3     }
      1  ThreadLocal.ThreadLocalMap threadLocals = null;

             可以看到thread 内部中持有TL的内部类变量。我们来看一下 ThreadLocalMap, threadLocalMap 内部定义一个类,Entry 类。这是threadLocalMap  内的变量

    ThreadLocalMap  类

      1 static class ThreadLocalMap {
      2     /**
      3      * The initial capacity -- MUST be a power of two.
      4      */
      5     private static final int INITIAL_CAPACITY = 16;
      6 
      7     /**
      8      * The table, resized as necessary.
      9      * table.length MUST always be a power of two.
     10      */
     11     private Entry[] table;
     12 
     13     /**
     14      * The number of entries in the table.
     15      */
     16     private int size = 0;
     17 
     18     /**
     19      * The next size value at which to resize.
     20      */
     21     private int threshold; // Default to 0
     22 }
     23 
      1  static class Entry extends WeakReference<ThreadLocal<?>> {
      2             /** The value associated with this ThreadLocal. */
      3             Object value;
      4 
      5             Entry(ThreadLocal<?> k, Object v) {
      6                 super(k);
      7                 value = v;
      8             }
      9 }

             我们看到 TL 的set 方法实际就是调用了 ThreadLocalMap 的set 方法。

      1  private void set(ThreadLocal<?> key, Object value) {
      2 
      3             // We don't use a fast path as with get() because it is at
      4             // least as common to use set() to create new entries as
      5             // it is to replace existing ones, in which case, a fast
      6             // path would fail more often than not.
      7 
      8             Entry[] tab = table;
      9             int len = tab.length;
     10             int i = key.threadLocalHashCode & (len-1);
     11 
     12             for (Entry e = tab[i];
     13                  e != null;
     14                  e = tab[i = nextIndex(i, len)]) {
     15                 ThreadLocal<?> k = e.get();
     16 
     17             	//找到相同的 key 
     18                 if (k == key) {
     19                     e.value = value;
     20                     return;
     21                 }
     22 
     23                 //某个key失效
     24                 if (k == null) {
     25                     replaceStaleEntry(key, value, i);
     26                     return;
     27                 }
     28             }
     29 
     30            	//走到这里必定是退出了循环,即是遇到空的 entry ,直接放在空的地方,检查是否需要扩容,重新 hash 
     31             tab[i] = new Entry(key, value);
     32             int sz = ++size;
     33             if (!cleanSomeSlots(i, sz) && sz >= threshold)
     34                 rehash();
     35 }
     36 
     37 
     38   //   这个方法是替代某些失效的entry ,最终的值会放在 table[staleSlot]
     39   //  slotToExpunge 这个变量从名字上可以看出就是需要擦洗的 slot (指的是某个位置)
     40  private void replaceStaleEntry(ThreadLocal<?> key, Object value,
     41                                        int staleSlot) {
     42             Entry[] tab = table;
     43             int len = tab.length;
     44             Entry e;
     45 
     46             // Back up to check for prior stale entry in current run.
     47             // We clean out whole runs at a time to avoid continual
     48             // incremental rehashing due to garbage collector freeing
     49             // up refs in bunches (i.e., whenever the collector runs).
     50             //  向前找是否有失效节点,如果有做一下标记,即是为 slotToExpunge 赋值
     51             int slotToExpunge = staleSlot;
     52             for (int i = prevIndex(staleSlot, len);
     53                  (e = tab[i]) != null;
     54                  i = prevIndex(i, len))
     55                 if (e.get() == null)
     56                     slotToExpunge = i;
     57 
     58             // Find either the key or trailing null slot of run, whichever
     59             // occurs first
     60             //  向后寻找是否有相同的 key
     61             for (int i = nextIndex(staleSlot, len);
     62                  (e = tab[i]) != null;
     63                  i = nextIndex(i, len)) {
     64                 ThreadLocal<?> k = e.get();
     65 
     66                 // If we find key, then we need to swap it
     67                 // with the stale entry to maintain hash table order.
     68                 // The newly stale slot, or any other stale slot
     69                 // encountered above it, can then be sent to expungeStaleEntry
     70                 // to remove or rehash all of the other entries in run.
     71             	//  找到相同的值,交换位置到 tab[staleSlot]
     72                 if (k == key) {
     73                     e.value = value;
     74 
     75                     tab[i] = tab[staleSlot];
     76                     tab[staleSlot] = e;
     77 
     78                     // Start expunge at preceding stale entry if it exists
     79                     // 擦洗失效值
     80                     if (slotToExpunge == staleSlot)
     81                         slotToExpunge = i;
     82                     cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
     83                     return;
     84                 }
     85 
     86                 // If we didn't find stale entry on backward scan, the
     87                 // first stale entry seen while scanning for key is the
     88                 // first still present in the run.
     89                 if (k == null && slotToExpunge == staleSlot)
     90                     slotToExpunge = i;
     91             }
     92 
     93             // If key not found, put new entry in stale slot
     94             //找不到值会放在 tab[staleSlot] ,即原来失效值的位置上
     95             tab[staleSlot].value = null;
     96             tab[staleSlot] = new Entry(key, value);
     97 
     98             // If there are any other stale entries in run, expunge them
     99             // 擦洗失效值
    100             if (slotToExpunge != staleSlot)
    101                 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    102 }
    103 

           

    TML

    TML2       

         可以看到我们在 set 的时候,TL内会检查是否存在失效值。也可以看到 ThreadLocalMap 的Hash 中解决冲突的方式只是简单的向下寻找空的位置,即线性探测,这样的效率比较低,所以建议 :

             每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

            下面看一下 get 方法,不难。

      1 // ThreadLocalMap
      2 private Entry getEntry(ThreadLocal<?> key) {
      3             int i = key.threadLocalHashCode & (table.length - 1);
      4             Entry e = table[i];
      5             if (e != null && e.get() == key)
      6                 return e;
      7             else
      8                 return getEntryAfterMiss(key, i, e);
      9 }
      1        private Entry getEntry(ThreadLocal<?> key) {
      2             int i = key.threadLocalHashCode & (table.length - 1);
      3             Entry e = table[i];
      4             if (e != null && e.get() == key)
      5                 return e;
      6             else
      7             	//获取的时候出现失效的entry
      8                 return getEntryAfterMiss(key, i, e);
      9         }
     10 
     11 
     12         // 往后找,失效的值擦洗掉,没有就返回 Null
     13         private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
     14             Entry[] tab = table;
     15             int len = tab.length;
     16 
     17             while (e != null) {
     18                 ThreadLocal<?> k = e.get();
     19                 if (k == key)
     20                     return e;
     21                 if (k == null)
     22                     expungeStaleEntry(i);
     23                 else
     24                     i = nextIndex(i, len);
     25                 e = tab[i];
     26             }
     27             return null;
     28         }

    使用ThreadLocal内存失效问题分析

    为什么使用弱引用

             我们来看一下weakReference 表示弱引用,java中有四种引用类型,强引用,弱引用,软引用,虚引用。

             在Java语言中, 当一个对象o被创建时, 它被放在Heap里. 当GC运行的时候, 如果发现没有任何引用指向o, o就会被回收以腾出内存空间. 也就是说, 一个对象被回收, 必须满足两个条件:

    • 没有任何引用指向它

    • GC被运行.

             

      1 DemoA a=new DemoA();
      2 DemoB b=new DemoB(a);

            假如有下面代码,如果我们增加一行代码来将a对象的引用设置为null,当一个对象不再被其他对象引用的时候,是会被GC回收的,但是对于这个场景来说,即时是a=null,也不可能被回收,因为DemoB依赖DemoA,这个时候是可能造成内存泄漏的。

      1 DemoA a=new DemoA();
      2 DemoB b=new DemoB(a);
      3 a=null;

             通过弱引用,有两个方法可以避免这样的问题。

      1 //方法1
      2 DemoA a=new DemoA();
      3 DemoB b=new DemoB(a);
      4 a=null;
      5 b=null;
      6 //方法2
      7 DemoA a=new DemoA();
      8 WeakReference b=new WeakReference(a);
      9 a=null;
     10 

            对于方法2来说,DemoA只是被弱引用依赖,假设垃圾收集器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的全都是弱引用),这时垃圾收集器会清除所有指向该对象的弱引用,然后把这个弱可达对象标记为可终结(finalizable)的,这样它随后就会被回收。

           我们可以设想b就是ThreadLocal ,试想一下如果这里没有使用弱引用,意味着ThreadLocal的生命周期和线程是强绑定,只要线程没有销毁,那么ThreadLocal一直无法回收。而使用弱引用以后,当ThreadLocal被回收时,由于Entry的key是弱引用,不会影响ThreadLocal的回收防止内存泄漏,同时,在后续的源码分析中会看到,ThreadLocalMap本身的垃圾清理会用到这一个好处,方便对无效的Entry进行回收。

            所以使用了弱引用的原因是为了防止 ThreadLocal 对象没有被正确回收而导致的内存泄漏。

    ThreadLocalMap 使用了弱引用会导致内存泄漏  

          ThreadLocalMap下文简称  TLM ,是存在Thread 中的,那么它的生存周期必定和线程的生命周期一样长的。

      1 static class Entry extends WeakReference<ThreadLocal<?>> {
      2             /** The value associated with this ThreadLocal. */
      3             Object value;
      4 
      5             Entry(ThreadLocal<?> k, Object v) {
      6                 super(k);
      7                 value = v;
      8             }
      9         }

             ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。(出处

    tlm

            

             我们从源码中也可以看到在 get 和 set 等方法都有检查失效值的操作,同时当我们使用TL时,某个线程不再需要某个值的时候应该调用 remove 方法,下面代码中 e.clear() 这一句实际是调用了弱引用的 clear 方法,实现对对象的回收。

      1         private void remove(ThreadLocal<?> key) {
      2             Entry[] tab = table;
      3             int len = tab.length;
      4             int i = key.threadLocalHashCode & (len-1);
      5             for (Entry e = tab[i];
      6                  e != null;
      7                  e = tab[i = nextIndex(i, len)]) {
      8                 if (e.get() == key) {
      9                     e.clear();
     10                     expungeStaleEntry(i);
     11                     return;
     12                 }
     13             }
     14         }
      1     /**
      2      * Clears this reference object.  Invoking this method will not cause this
      3      * object to be enqueued.
      4      *
      5      * <p> This method is invoked only by Java code; when the garbage collector
      6      * clears references it does so directly, without invoking this method.
      7      */
      8     public void clear() {
      9         this.referent = null;
     10     }

    其实我们从源码分析可以看到,ThreadLocalMap是做了防护措施的

    • 首先从ThreadLocal的直接索引位置(通过

      ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e

    • 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询

            由上面的分析,我们知道了ThreadLocal 使用了弱引用后还是会导致内存泄漏,而内存泄漏的原因是 : ThreadLocalMap的生存周期和线程一样长,而不是使用弱引用导致的!!!         

    Entry 的 Hash 值

             如何实现一个线程多个ThreadLocal对象,每一个ThreadLocal对象是如何区分的呢? 

      1 void createMap(Thread t, T firstValue) {
      2        t.threadLocals = new ThreadLocalMap(this, firstValue);
      3 }
      1 static class ThreadLocalMap {
      2      static class Entry extends WeakReference<ThreadLocal<?>> {
      3 
      4        /** The value associated with this ThreadLocal. */
      5         Object value;
      6 
      7         Entry(ThreadLocal<?> k, Object v) {
      8                 super(k);
      9                value = v;
     10         }
     11      }
     12 
     13      ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
     14 	   //构造一个Entry数组,并设置初始大小
     15            table = new Entry[INITIAL_CAPACITY];
     16            //计算Entry数据下标
     17            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
     18 	   //将`firstValue`存入到指定的table下标中
     19            table[i] = new Entry(firstKey, firstValue);
     20            size = 1;//设置节点长度为1
     21            setThreshold(INITIAL_CAPACITY); //设置扩容的阈值
     22       }
     23 //...省略部分代码
     24 }
     25 
     26 
      1 private final int threadLocalHashCode = nextHashCode();
      2 private static AtomicInteger nextHashCode = new AtomicInteger();
      3 private static final int HASH_INCREMENT = 0x61c88647;
      4 
      5 private static int nextHashCode() {
      6     return nextHashCode.getAndAdd(HASH_INCREMENT);
      7 }

            那为什么要使用到 0x61c88647 这个值呢? 我们首先要明白一点,散列的目的是使数据分布更加均匀。那么这个数字的使用必定会达到这个目的。

    魔数0x61c88647的选取和斐波那契散列有关,0x61c88647对应的十进制为1640531527。而斐波那契散列的乘数可以用 (long)((1L<<31)*(Math.sqrt(5)-1)); 如果把这个值给转为带符号的int,则会得到-1640531527。也就是说(long)((1L<<31)*(Math.sqrt(5)-1));得到的结果就是1640531527,也就是魔数0x61c88647

          

    建议

    • 将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露

    • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

    • 在线程池中使用ThreadLocal ,有可能会出现数据混淆的情况,原因是数据没及时清理,线程放回线程池中又被拿出来使用。

     

    总结

    • ThreadLocal 使用 弱引用的原因是为了处理非常大和生命周期非常长的线程,为了防止 ThreadLocal回收导致的内存泄漏,
      但是使用了弱引用也有可能导致内存泄漏,这次 ThreadLocal被回收了,但是value一直存活着,要是没有手动删除的话,
      依旧会导致内存泄漏,所以ThreadLocalMap 的 get ,set 中都有防护措施---检查ThreadLocal 为空的 Entry ,然后删除掉该
      Entry.
    • 在使用完ThreadLocal,记得调用remove 方法。

    参考资料 :

  • 相关阅读:
    训练20191009 2018-2019 ACM-ICPC, Asia East Continent Finals
    [学习笔记] 有上下界网络流
    [HDU4609] 3-idiots
    [HDU1402] A * B Problem Plus
    [HNOI2017] 礼物
    训练20191007 2017-2018 ACM-ICPC Latin American Regional Programming Contest
    [ZJOI2014] 力
    训练20191005 2017-2018 ACM-ICPC Asia East Continent League Final
    [一本通学习笔记] 树链剖分
    [一本通学习笔记] 快速幂
  • 原文地址:https://www.cnblogs.com/Benjious/p/10162864.html
Copyright © 2011-2022 走看看