java 并发--- ThreadLocal

Posted float123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 方法。

 

参考资料 :

以上是关于java 并发--- ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章

Java并发:ThreadLocal的简单介绍

Java并发:ThreadLocal的简单介绍

Java并发:ThreadLocal的简单介绍

Java并发:ThreadLocal的简单介绍

并发编程的挑战(Java并发编程的艺术)

Java并发编程:深入剖析ThreadLocal