美文网首页Java笔试面试
ThreadLocal详细介绍+源码分析

ThreadLocal详细介绍+源码分析

作者: 小北觅 | 来源:发表于2018-08-27 19:19 被阅读40次

    前言:本篇文章要研究的是ThreadLocal 这个类,基于JDK1.8进行源码分析。ThreadLocal 主要用来提供线程局部变量,也就是变量只对当前线程可见。这个类也是面试中出现率很高的问题,特此整理一番。

    一、ThreadLocal源码分析

    1.1 什么是ThreadLocal

    首先,它是一个数据结构,有点像HashMap,可以保存”key : value”键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。

    ThreadLocal<String> localName = new ThreadLocal<>();
    localName.set("threadlocal");
    String name = localName.get();
    

    在线程1中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值threadlocal,同时在线程1中通过localName.get()可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。 因为如前言中提到的,ThreadLocal存储的变量只对当前线程可见。

    1.2 源码起飞

    那么这是如何实现的呢?让我们进入美滋滋的欣赏源码环节:

    首先来看set()方法:

    public void set(T value) {
        // 得到当前线程对象
        Thread t = Thread.currentThread();
        //获取当前线程中的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //如果map存在,则将当前ThreadLcoal对象作为key,要存储的对象作为value存到map里面去
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    

    note:map.set(this,value)由于是在ThreadLocal类中调用,所以this指的就是ThreadLocal对象。

    接下来看getMap(Thread t)方法:

    //就是返回线程中的threadLocals  
    ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
    }
    

    我们进到threadLocals的声明处,他是在Thread类下:
    ThreadLocal.ThreadLocalMap threadLocals = null;
    这时我们遇到了另一个类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) {
                   super(k);
                   value = v;
               }
           }
        
        ........省略其它
    }
    

    通过查看源码我们可以发现的是ThreadLocalMapThreadLocal的一个内部类。 用Entry类来进行存储。 我们的值都是存储到这个Map上的,key是当前ThreadLocal对象!上面还涉及到WeakReference<T>(弱引用),有关弱引用见相关博客。

    继续往下走,如果map不为空,则把值放入map中。看一下set(ThreadLocal<?> key, Object value)方法。

    这个方法是由map调用,故为ThreadLocalMap类中的方法:

    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();
    }
    

    这个方法的作用就是把key(当前的ThreadLocal对象)和value(我们要存储的值)存入Entry[] tab中。

    继续往下看,如果map为空,则创建一个map。即调用createMap(Thread t, T firstValue)方法。

    该方法是ThreadLocal类中的方法。源码如下:

    void createMap(Thread t, T firstValue) {
           t.threadLocals = new ThreadLocalMap(this, firstValue);
       }
    

    又调用到了ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)方法

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }
    //INITIAL_CAPACITY=16 必须是2的整数倍
    

    同时又调了setThreshold(int len)方法;

    //使resize的阈值为2/3的负载因子
    private void setThreshold(int len) {
              threshold = len * 2 / 3;
          }
    

    所以我们知道会有resize这样的方法来扩充table的容量。也确实有:

    //使table的容量变为原来的2倍
    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;
          }
    

    我们想知道谁调用了resize()呢。通过查看调用关系(Eclipse下的快捷键是Ctrl+Alt+H),发现是rehash()方法调用的resize()。

     /**
           * Re-pack and/or re-size the table. First scan the entire
           * table removing stale entries. If this doesn't sufficiently
           * shrink the size of the table, double the table size.
           */
    private void rehash() {
              //擦除陈腐的Entries
              expungeStaleEntries();
    
              // Use lower threshold for doubling to avoid hysteresis
              if (size >= threshold - threshold / 4)
                  resize();
          }
    

    那谁又调了rehash()呢,我们发现是老熟人set(ThreadLocal<?> key, Object value)。至此恍然大悟,当我们向map中set数据时,最终存到的是ThreadLocal的内部类ThreadLocalMap中的Entry[] table中。并且这个table有扩容和清理无用对象的机制。

    1.3 小总结

    • 每个Thread维护着一个ThreadLocalMap的引用。
    • ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。
    • 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象。
    • 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象。
    • ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

    二、避免内存泄漏

    我们来看一下ThreadLocal的对象关系引用图:


    001.png

    ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key,就会导致value永远访问不到,从而导致内存泄漏。

    想要避免内存泄露就要手动remove()掉!

    参考资料:

    ThreadLocal就是这么简单

    相关文章

      网友评论

        本文标题:ThreadLocal详细介绍+源码分析

        本文链接:https://www.haomeiwen.com/subject/zjitwftx.html