java 并发(七)--- ThreadLocal

2018-12-24 09:09:43来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

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

 

ThreadLocal 概述    

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

ReentranReadWriteLock中。

        ThrealLocal

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

 

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  内的变量

  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         }

 

ThreadLocalMap 的 key 失效

           ThreadLocalMap下文简称  TLM 。

  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         }

         可以看到 Entry 继承 WeakReference (弱引用)。ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,于是这就存在一条强引用链的关系一直存在: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     }

          

         我们来看一下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进行回收。

 

 

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

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

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

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

 

 

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-面试必问深度解析
  • JAVA高级架构 微信公众号的 “ThreadLocal的使用及原理分析”
  • 从 ThreadLocal 的实现看散列算法

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:SpringBoot(十八)@value、@Import、@ImportResource、@PropertyS

下一篇:Java开发笔记(四十)日期与字符串的互相转换