ThreadLocal源码分析

什么是ThreadLocal?

  • 可以看做线程内局部变量,也就是线程间隔离,仅在当前线程范围内有效。比如我们通常都会定义全局范围内的普通变量
    全局范围变量就是临界资源,那就会有多线程安全问题;但是如果定义的是ThreadLocal变量,每个线程都持有参数副本,各自间互不妨碍,也就没有多线程问题

什么时候用ThreadLocal?

  • 比如在同一个线程内 不同逻辑间需要共享数据(但又无法通过传值来共享数据),或者在同一个线程内为避免重复创建对象 而希望数据重用等情况.

如何使用ThreadLocal?

  • 使用比较简单,但需要养成一个好习惯,每次使用结束都手动remove参数,具体原因文末解释

    public class ThreadLocalExample {
    
       private ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
       @Test
       public void testBasic() {
           try {
               String exist = threadLocal.get();
               assertNull(exist);
    
           threadLocal.set("hhhhh-10");
           assertEquals("hhhhh-10", threadLocal.get());
       } finally {
           threadLocal.remove();
       }
    

    }

ThreadLocal、ThreadLocalMap、Thread三者间的关系

  • ThreadLocalMap是ThreadLocal的内部类
  • Thread内部维护了一个ThreadLocalMap对象,维护在Thread实例中 <ThreadLocal, ValueObject>的关系
  • ThreadLocal的set,get,remove等操作都依托于ThreadLocalMap完成
  • ThreadLocalMap维护信息:
    在这里插入图片描述

ThreadLocal原理分析:

  • 内部类ThreadLocalMap是一个Map结构,以数组的形式(tab)存取entry(key,value),与HashMap不同,解决哈希冲突采用的是线性探测法
  • 开放定址法: 通过开放定址法解决hash冲突的方式有 线行探测法、平方探查法、双散列函数探查法,其中线行探测法是最简单的一种,ThreadLocal就使用的这种方式
  • ThreadLocalMap#Entry中的key定义的是虚引用,便于GC回收
  • 既然用的虚引用,肯定存在key过期待清理entry,ThreadLocal采用的惰性清理,也就是在真正使用的时候才去尝试清理

ThreadLocal源码分析

ThreadLocal内部结构及主要方法
在这里插入图片描述

为方便描述: tab中每个index对应的位置称为槽(slot), 没有Entry的槽称为空槽,槽中有数据但是key为空(被GC了)称为过期槽,剩下的槽中有数据, key也存在 称为值槽
set方法
  public void set(T value) {
      // 获取到当前线程
      Thread t = Thread.currentThread();
      // 从当前线程中去找到由Thread自身维护的ThreadLocalMap映射
      ThreadLocalMap map = getMap(t);
      if (map != null)
          // 如果找到了就把值加进去
          map.set(this, value);
      else
          // 否则的话 就创建一个ThreadLocalMap, 然后放到Thread中
          createMap(t, value);
  }      
   void createMap(Thread t, T firstValue) {
       t.threadLocals = new ThreadLocalMap(this, firstValue);
   }
    /**
     * 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.
     */
     // 意思是说,ThreadLocalMaps是懒加载模式,正在使用的时候才会创建ThreadLocalMaps
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        // 找到对应的槽,因为是首次创建map, i对应的肯定是空槽,直接把数据放进去就行
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        // 设置tab的阈值,超过这个值就需要扩容了(毕竟是hash表,不能装的太满)
        setThreshold(INITIAL_CAPACITY);
        
    }           

     /**
      * Set the resize threshold to maintain at worst a 2/3 load factor.
      */
     private void setThreshold(int len) {
     
         threshold = len * 2 / 3;
         
     } 
     
     //
get方法
  public T get() {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null) {
          // 从tab表中找到key=当前threadlocal对象的entry
          ThreadLocalMap.Entry e = map.getEntry(this);
          // 如果找到了直接返回
          if (e != null) {
              @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
          }
      }
      // 没找到就初始化赋个值,
      return setInitialValue();
  }
  
   private T setInitialValue() {
       // 给value一个初始值
       T value = initialValue();
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);
       // 初始化有两种情况
       // 一种是刚开始map还没创建,那就创建map,然后把entry<当前threadLocal, value>放进去
       // 另一种是map已经创建好了,直接把entry<当前threadLocal, value>放进就行了
       if (map != null)
           map.set(this, value);
       else
           createMap(t, value);
       return value;
   }
   
   // 默认值null
   protected T initialValue() {
       return null;
       
   }       
   
   // 
remove方法
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        // 直接依托给threadLocalMap删除
        if (m != null)
            m.remove(this);
    }   

ThreadLocalMap

为上层应用(ThreadLocal)提供底层细节实现,一起来看看吧

set方法
      private void set(ThreadLocal<?> key, Object value) {  
          Entry[] tab = table;
          int len = tab.length;
          // 计算当前key对应的槽
          int i = key.threadLocalHashCode & (len-1);

          // 想想为什么需要用for循环?
          // 前面提到解决hash冲突采用的是线性探测法,也就是如果i对应的槽有值,那就只能尝试下一个槽是否可用,以此类推~
          // 当然这里操作不仅仅是添加值,还会清理过期槽
          // 这里遍历到是空槽的地方就结束了,因为如果已经到空槽了都没找到合适的entry,说明这个key对应的entry肯定不存在
          for (Entry e = tab[i];
               e != null;
               e = tab[i = nextIndex(i, len)]) {
              // 看看Entry定义可以发现 e.get()拿到的就是key 
              ThreadLocal<?> k = e.get();

              // 如果正好匹配了,那就用新值替换旧值
              if (k == key) {
                  e.value = value;
                  return;
              }
              // 存在entry,但是key为null,说明是过期槽,那就把这个槽中给的entry替换掉
              if (k == null) {
                  replaceStaleEntry(key, value, i);
                  return;
              }
          }

          // 走到这里说明hash表中没有,创建entry,然后放进去
          // 这里的i是上面遍历结束的空槽哦
          tab[i] = new Entry(key, value);
          int sz = ++size;
          
          // 如果这里尝试清理了过期槽发现还是超过了阈值,那就进行一次扩容
          if (!cleanSomeSlots(i, sz) && sz >= threshold)
              rehash();
              
      }
      
      // 
nextIndex方法,就是找到下一个槽
     private static int nextIndex(int i, int len) {
         return ((i + 1 < len) ? i + 1 : 0);
     }     
replaceStaleEntry方法 替换过期槽中的entry

这里需要考虑一个问题,因为采用的线性探测法保存entry,并且entry中的key采用的虚引用(在没有强引用的情况下,存活周期为一次GC间隔),可能被GC,从而这个entry就会被清理掉

假设:key1经过计算对应的是槽3,但由于hash冲突存储的时候已经被顺位到了槽5,这时槽3,4,5都是值槽,但由于某种原因,槽3 key过期被清理后变成了空槽
这里的操作是需要将槽4,槽5向前挪动一个位置,从而变成槽3槽4为值槽,槽5为空槽。如果不这样处理的话,会导致一个问题:
某个时刻 我需要得到key1对应的entry,经过计算对应的是槽3,取的时候发现槽3为空槽,那就说明不存在key1对应的这个entry直接返回了,而实际上呢,这个entry在槽5的位置上

这个方法就会做这样一个挪动的操作,并且它会从指定的staleSlot槽的位置向前向后扫描,直到遇到空槽为止,将这扫描到的连续空间
进行过期槽的清除,并伴随挪动操作

     private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                    int staleSlot) {
         Entry[] tab = table;
         int len = tab.length;
         Entry e;
         // slotToExpunge这个参数就是来确定要进行清除操作的槽的起始位置
         int slotToExpunge = staleSlot;
         // 向前扫描,扫到空槽为止
         for (int i = prevIndex(staleSlot, len);
              (e = tab[i]) != null;
              i = prevIndex(i, len))
             if (e.get() == null)
                 slotToExpunge = i;

         // 向后扫描,直到key匹配或者空槽为止
         for (int i = nextIndex(staleSlot, len);
              (e = tab[i]) != null;
              i = nextIndex(i, len)) {
             ThreadLocal<?> k = e.get();
             
             // 如果正好key对应的上,新值替换旧值,并且交换位置(完成了替换和挪动操作)
             // 这样待清理的staleSlot槽上的entry就被换到了后面位置,等待后面清除
             if (k == key) {
                 e.value = value;
                 tab[i] = tab[staleSlot];
                 tab[staleSlot] = e;

                 // 如果staleSlot前面第一个就是空槽,说明前面没有可清理的entry,正好原staleSlot槽上的元素向后挪了一个位置,
                 // 那就从那个位置开始尝试清理entry
                 if (slotToExpunge == staleSlot)
                     slotToExpunge = i;
                 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                 return;
             }
             // 如果此槽是过期槽,并且staleSlot槽前面没有需要清理槽,
             // 那就暂时修改 "开始清理的槽"的起始位置(for没结束,就有可能修改) 为此槽的位置
             if (k == null && slotToExpunge == staleSlot)
                 slotToExpunge = i;
         }
         // If key not found, put new entry in stale slot
         tab[staleSlot].value = null; // help gc
         tab[staleSlot] = new Entry(key, value);

         // 如果有过期槽就进行清理
         if (slotToExpunge != staleSlot)
             cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
             
         // 可以看到 staleSlot的值始终没变过,说明不管是替换,还是新加都在这个staleSlot槽位置上进行
         
         // 而 slotToExpunge 不过是在确定要开始进行清理的起始位置
     
     }  
         
     // 
     //         
prevIndex方法 和nextIndex相反,找到前一个槽
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
expungeStaleEntry方法 真正清理过期槽的entry的方法

我们在清理了特定槽上的entry后,需要考虑后面的槽应该所在的位置(贯穿始终的线性探测,顺位问题)

       private int expungeStaleEntry(int staleSlot) {
           Entry[] tab = table;
           int len = tab.length;

           // expunge entry at staleSlot
           // 将这个槽entry的值置为null, 同时将此槽置为空槽
           tab[staleSlot].value = null;
           tab[staleSlot] = null;
           size--;

           // Rehash until we encounter null
           Entry e;
           int i;
           for (i = nextIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = nextIndex(i, len)) {
               ThreadLocal<?> k = e.get();
               // 如果key被GC了,直接清理
               if (k == null) {
                   e.value = null;
                   tab[i] = null;
                   size--;
               } else {
                   // 否则的话我们就需要判断是否需要此entry是否需要改变槽的位置了
                   // 重新计算它应该所在的槽的位置,如果和它目前所在的槽的位置不一致,要尝试进行挪到它最合适的位置
                   int h = k.threadLocalHashCode & (len - 1);
                   if (h != i) {
                       tab[i] = null;

                       // Unlike Knuth 6.4 Algorithm R, we must scan until
                       // null because multiple entries could have been stale.
                       // 也就是线性探测,不断尝试
                       while (tab[h] != null)
                           h = nextIndex(h, len);
                       tab[h] = e;
                   }
               }
           }
           return i;
       } 
        
       // 
cleanSomeSlots方法

顾名思义,清除部分槽上的entry,根据文档解释,不用每次都清理所有entry,只尝试log2(n)次清理,这是实验过 性能上更优
对于惰性清除这种,应该很多地方都有这种思想,每次只清理部分数据,避免单次耗时过长;
Guava Cache过期缓存清理也是这种思路。

    /**
     * Heuristically scan some cells looking for stale entries.
     * This is invoked when either a new element is added, or
     * another stale one has been expunged. It performs a
     * logarithmic number of scans, as a balance between no
     * scanning (fast but retains garbage) and a number of scans
     * proportional to number of elements, that would find all
     * garbage but would cause some insertions to take O(n) time.
     *
     * @param i a position known NOT to hold a stale entry. The
     * scan starts at the element after i.
     *
     * @param n scan control: {@code log2(n)} cells are scanned,
     * unless a stale entry is found, in which case
     * {@code log2(table.length)-1} additional cells are scanned.
     * When called from insertions, this parameter is the number
     * of elements, but when from replaceStaleEntry, it is the
     * table length. (Note: all this could be changed to be either
     * more or less aggressive by weighting n instead of just
     * using straight log n. But this version is simple, fast, and
     * seems to work well.)
     *
     * @return true if any stale entries have been removed.
     */
    // 插入元素时传入的n=元素个数
    // 替换元素时传入的n=table的长度
    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
            if (e != null && e.get() == null) {
                n = len;
                removed = true;
                i = expungeStaleEntry(i); // 给一个起始位置,一次性清理连续一段
            }
        } while ( (n >>>= 1) != 0); // log2(n)次清理
        return removed;
    }
rehash方法

rehash比较简单,rehash之前先进行一次全局清理expungeStaleEntries

再rehash过程中遇到过期槽,也直接清理掉

    private void rehash() {
        expungeStaleEntries();

        // Use lower threshold for doubling to avoid hysteresis
        if (size >= threshold - threshold / 4)
            resize();
    }

     * Expunge all stale entries in the table.
     */
    private void expungeStaleEntries() {
        Entry[] tab = table;
        int len = tab.length;
        for (int j = 0; j < len; j++) {
            Entry e = tab[j];
            if (e != null && e.get() == null)
                expungeStaleEntry(j);
        }
    }
    
    /**
     * Double the capacity of the table.
     */
     // 双倍扩容
    private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;
        Entry[] newTab = new Entry[newLen];
        int count = 0;

        for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null; // Help the GC
                } else {
                    // 计算槽位,线性探测,顺位放入等等.
                    int h = k.threadLocalHashCode & (newLen - 1);
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);
                    newTab[h] = e;
                    count++;
                }
            }
        }

        // 设置新的阈值
        setThreshold(newLen);****
        size = count;
        
        table = newTab;
        
    }                       
getEntry
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        // 如果找到则直接返回
        if (e != null && e.get() == key)
            return e;
        else
            // 因为线性探测,往后顺位存值这种方式,可能不能一次取到key相等的entry
            // 那就尝试顺位向后取
            return getEntryAfterMiss(key, i, e);
    }       
    
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;

        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                return e;
            if (k == null) // 清理掉过期槽中的entry
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        
        return null;
        
    }              
remove方法

清除指定key对应的entry

    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;
            }
            
        }
        
    }
ThreadLocalMap结构
static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            // 可见key定义成了虚引用
            super(k);
            value = v;
            
        }
        
    }        

以上便是ThreadLocal核心源码,下面来看看ThreadLocal使用过程中带来的问题

ThreadLocal常见问题

  • 内存泄漏
    • threadLocal的内存泄漏主要是因为,key是弱引用被回收了之后,所对应的value并未被回收(仍被entry持有)
    • 虽然threadLocal在内部使用了惰性清理的方式,但只有在调用了get,set,remove等操作才会触发清理
    • 所以要处理这个情况,只需要在用完之后记得手动remove即可,养成一个好的编程习惯
  • ThreadLocalMap中key被定义为虚引用(存活周期为一次GC间隔),在使用threadLocal期间发生了GC,key是否会被GC掉?
    • 不会,虚引用所引用的对象被GC回收的规则是,如果只有一个虚引用 那么在下一次被GC的时候会被回收,如果此对象存在强引用,GC时是不会被GC的
    • 这里比如在开始的例子中,只要创建threadLocal的对象A还存活或者说没有释放threadLocal,也就是说A还持有threadLocal的强引用,因此GC时也不会被回收
    • 此threadLocal的虚引用存在于用到过threadLocal的thread的threadLocalMap中
  • 在线程池中使用threadLocal后没有remove的后果
    • 线程池中核心线程不会销毁,也就是在复用过程中如果没有remove,那么下一次再次使用此线程的时候,其值仍然存在

    • 养成好习惯,使用完之后remove也就没事了~

    • 来看一个例子

        public class ThreadLocalExample {
      
        private ThreadLocal<String> threadLocal = new ThreadLocal<>();
        private ExecutorService executor = Executors.newFixedThreadPool(1);
      
        @Test
        public void testWrong() {
            List<Future> futures = Lists.newArrayList();
            IntStream.rangeClosed(0, 5).forEach(i -> {
                Future<?> future = executor.submit(() -> {
                    String exist = threadLocal.get();
                    if (Strings.isNullOrEmpty(exist)) {
                        threadLocal.set("hihihi-" + i);
                    }
      
                    System.out.println(threadLocal.get());
                });
      
                futures.add(future);
            });
      
            futures.forEach(future -> {
                try {
                    Uninterruptibles.getUninterruptibly(future);
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            });
      
            /**
             * sout:
             *
             * hihihi-0
             * hihihi-0
             * hihihi-0
             * hihihi-0
             * hihihi-0
             * hihihi-0
             */
        }
      
        @Test
        public void testRight() {
            List<Future> futures = Lists.newArrayList();
            IntStream.rangeClosed(0, 5).forEach(i -> {
                Future<?> future = executor.submit(() -> {
      
                try {
                    String exist = threadLocal.get();
                    if (Strings.isNullOrEmpty(exist)) {
                        threadLocal.set("hihihi-" + i);
                    }
      
                    System.out.println(threadLocal.get());
                } finally {
                    // 记得remove
                    threadLocal.remove();
                }
            });
      
            futures.add(future);
        });
      
        futures.forEach(future -> {
            try {
                Uninterruptibles.getUninterruptibly(future);
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });
      
        /**
         * sout:
         *
         * hihihi-0
         * hihihi-1
         * hihihi-2
         * hihihi-3
         * hihihi-4
         * hihihi-5
         *
         */
         
       }
      

以上是个人理解,如有问题请指出,谢谢!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柏油

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值