ThreadLocal代码浅析

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

是什么

ThreadLocal是一个比较老的类了,确切地说,在JUC之前就已经存在了。

  • 第一次出现是在JDK1.2的时候。

ThreadLocal提供的是一个 线程本地变量,作用可以如此描述:

同一个ThreadLocal对象,对于不同的线程,可见的变量始终是该线程上做出所有修改最新结果,和其他线程的修改无关,优点类似与于线程的私有变量。

我们使用了全局的线程池,做了以下的demo来看ThreadLocal的特性:

 public class ThreadLocalTest {
 ​
     private static ThreadLocal<Integer> localI = new ThreadLocal<>();
     
     static{
         localI.set(1);
     }
 ​
     private static void func1(){
         GlobalThreadPool.execute(()->{
             System.out.println("func1:first:"+localI.get());
             localI.set(0);
             System.out.println("func1:set and sleep");
             try {
                 Thread.sleep(100);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println("func1:second:"+localI.get());
         });
     }
 ​
     private static void func2(){
         GlobalThreadPool.execute(()->{
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println("func2:first:"+localI.get());
             localI.set(10000);
             System.out.println("func2:second:"+localI.get());
         });
     }
 ​
     public static void main(String[] args) {
         func1();
         func2();
     }
     
 }
 ​
复制代码

我们先让第一个方法初始化localI并初始化;随后第二个方法对Thread对象进行读写。

结果:

func1:first:null func1:set and sleep func1:second:0 func2:first:null func2:second:10000

此处可以看到:

  • func2中的变量并没有受到func1中修改的影响。

因此,ThreadLocal可以用在一些需要线程提供隔离环境,但又需要公共变量统一维护的地方使用。

如何做到

“见人说人话,见鬼说鬼话”(根据不同的特征值获取不同的已存的对象),这种功能我们一般会联想到Map映射的相关功能。我们知道java中的线程是有一个唯一线程ID的,那么如果我们使用相同的思路:

  • 使用线程ID作为key
  • 将线程的数据作为value

那么我们是否可以做到相同的功能,每次存取都是独立的?

答案是肯定的,源码中的实现也是使用了类似的思路。

ThreadLocal源码相关设计

相关数据的维护 – 存取

  • ThreadLocal内部实现了一个简单的定制版本hashMap,来作为存储线程局部变量的结构。
 static class ThreadLocalMap { //....
 }
复制代码
  • Thread内部包括了一个ThreadLocalMap的变量,来作为线程的本地存储。

     public
     class Thread implements Runnable {
         //............
     ​
     /* ThreadLocal values pertaining to this thread. This map is maintained
          * by the ThreadLocal class. */
         ThreadLocal.ThreadLocalMap threadLocals = null;
     }
    复制代码

    确切地说,ThreadLocal只是维护线程自身的相关私有变量,本身并不负责存储该部分信息。

看我们使用最频繁的两个方法:set和get,能够证明我们的看法是正确的。

  public T get() {
         Thread t = Thread.currentThread();
         ThreadLocalMap map = getMap(t);
         if (map != null) {
             ThreadLocalMap.Entry e = map.getEntry(this);
             if (e != null) {
                 @SuppressWarnings("unchecked")
                 T result = (T)e.value;
                 return result;
             }
         }
         return setInitialValue();
     }
 ​
     public void set(T value) {
         Thread t = Thread.currentThread();
         ThreadLocalMap map = getMap(t);
         if (map != null)
             map.set(this, value);
         else
             createMap(t, value);
     }
复制代码

数据存储单元

数据清理

  • 源码在实现角度,考虑了后续处理的问题:

    • 线程退出的时候,需要将线程的变量销毁,否则会占用不必要空间。

我们可以通过弱引用(weakReference)来进行数据退出时的销毁。

ThreadLocal的内部使用的存储单元 entry,继承并扩展了 WeakReference,来做到类似的功能:

         static class Entry extends WeakReference<ThreadLocal<?>> {
             /** The value associated with this ThreadLocal. */
             Object value;
 ​
             Entry(ThreadLocal<?> k, Object v) {
                 super(k);
                 value = v;
             }
         }
复制代码

我们知道:

  • weakReference在下一次全局GC时,如果没有被强引用的地方,那么会被清理掉

那么,一旦线程退出,就意味着该entry是无法再次被访问到了,那么自然而然会被JVM清理掉。

数据存储

在这部分我们对比HashMap中的Entry,可以看到此处的Entry并没有设置前后指针的设定,那么存储是如何进行的呢?我们看相关的get/set源码,其实此处的实现很简单。

         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) {
                     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();
         }
 ​
 ​
 ​
 ​
 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
                 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)
                     expungeStaleEntry(i);
                 else
                     i = nextIndex(i, len);
                 e = tab[i];
             }
             return null;
         }
复制代码

在set中我们可以看到:如果发生了hash碰撞,Map会根据情况,做三个操作:

  • 如果hash碰撞索引处的原来的entry:

    • key不为空且和我们需要写入的key不同

      • 此时取下一个索引位置
    • key不为空,和我们写入的key相同

      • 替换,返回
    • key为空,那么进行 replaceStaleEntry的操作,在这个地方源码会将对应的hash中的value清理掉,随后将我们要设置的value填上去。

    没有注意到的地方

我们知道线程是需要考虑重入性的,ThreadLocal有相关API能够支持这个特性。

JDK1.2中考虑到了该情况,因此实现了一个子类ThreadLocal的子类InheritableThreadLocal,来提供这一特性。

写了一个如下的demo,来测试该类的重入性以及可见性:

         new Thread(()->{
             InheritableThreadLocal <Integer> inheritLocal = new InheritableThreadLocal();
             inheritLocal.set(10);
             System.out.println("func3:value"+inheritLocal.get());
             new Thread(()-> {
                 System.out.println("func3-ChildThread1:value:" + inheritLocal.get());
                 inheritLocal.set(1000);
                 System.out.println("func3-ChildThread1:value:" + inheritLocal.get());
             }).start();
             new Thread(()-> {
                 System.out.println("func3-ChildThread2:value:" + inheritLocal.get());
                 try {
                     Thread.sleep(100);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("func3-ChildThread2:value:" + inheritLocal.get());
                 try {
                     Thread.sleep(5000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("func3-ChildThread2:value:" + inheritLocal.get());
             }).start();
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println("func3:value:" + inheritLocal.get());
             inheritLocal.set(50);
             System.out.println("func3:value:" + inheritLocal.get());
         }).start();
复制代码

输出:

 func3:value10
 func3-ChildThread1:value:10
 func3-ChildThread1:value:1000
 func3-ChildThread2:value:10
 func3-ChildThread2:value:10
 func3:value:10
 func3:value:50
 func3-ChildThread2:value:10
复制代码

我们得出以下结论:

  • 子线程可以获得在进入线程之前最新更新的值
  • 在子线程开始之后,父子线程所获取的值就是独立的了,并不会相互影响,相当于线程开始时获取了一个最新的值作为初始值。

查看源码,可以看到InheritableThreadLocal内部重写了ThreadLocal的两个方法做了快照以及替换:

 ThreadLocalMap getMap(Thread t) {
        return t.inheritableThreadLocals;
     }
 ​
 void createMap(Thread t, T firstValue) {
         t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
     }
复制代码

同样的,线程内部也维护了一个inheritableThreadLocals的变量。

ThreadLocal的内存泄露问题

内存泄漏:占用但无法回收

Thread中维护了一个ThreadLocalMap,key是弱引用。

引用ThreadLocal的强引用消失了,因为key是弱引用因此ThreadLocal会被回收。

会导致ThreadLocalMap中的key变成了null,但value被强引用指向了某个线程变量,value会一直回收不了。

  • 为什么key使用弱引用?

    • 如果是强引用,那么ThreadLocal持有了ThreadLocal的强引用,那么ThreadLocal无法回收。
    • 使用弱引用,ThreadLocal也会被回收,在下一次ThreadLocalMap使用时就会被清除。

内存泄漏根源:

ThreadLocalMap维护在ThreadLocal,生命周期和Thread一样长;同时Thread

正确使用方法:

  • 使用完ThreadLocal使用完调用remove方法进行清除。
  • 或将ThreadLocal定义为private static,一直存在强引用,任何时候都可以通过ThreadLocal的弱引用key访问到entry的value值,进而清除。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享