Java笔试面试

ThreadLocal详细介绍+源码分析

2018-08-27  本文已影响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 小总结

二、避免内存泄漏

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


001.png

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

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

参考资料:

ThreadLocal就是这么简单

上一篇下一篇

猜你喜欢

热点阅读