Java面试题之ThreadLocal

1、ThreadLocal是什么?

ThreadLocal类并不是用来解决多线程环境下的共享变量问题,而是用来提供线程内部的共享变量,在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。在线程中,可以通过get()/set()方法来访问变量。ThreadLocal实例通常来说都是private static类型的,它们希望将状态与线程进行关联。这种变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

2、ThreadLocal如何保证各个线程的数据互不干扰?

先看下ThreadLocal类的get()和set()方法:

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

可以看到,每个线程都有一个叫ThreadLocalMap的数据结构,根据当前线程对象,调用getMap(Thread t)获取线程对应的ThreadLocalMap对象。

threadLocals是Thread类的成员变量,初始化为null,如下:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

而getMap(Thread t)方法就是获取这个线程的ThreadLocalMap,如下:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

因此从上述代码可以发现:当执行set方法时,其值是保存在当前线程的threadLocals变量中;当执行get方法时,是从当前线程的threadLocals变量获取。
所以线程A 执行set的值,线程B是无法通过get方法获取到的。

3、什么是ThreadLocalMap以及其结局hash冲突的方式

首先从字面上看,ThreadLocalMap和HashMap很相似,它具有HashMap的部分特性,比如容量、扩容阈值等。但ThreadLocalMap内部通过Entry类(ThreadLocalMap类的静态内部类)来存储key和value,Entry类的定义为:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

由上可知:Entry继承自WeakReference,通过 super(k); 语句可以知道,ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。

ThreadLocalMap解决hash冲突:

与 HashMap 不同,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。具体实现为如下两个方法:

//ThreadLocalMap中的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)]) {// 调用了nextIndex()方法,解决hash冲突
        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();
     }

/**
  * Increment i modulo len.
  */
private static int nextIndex(int i, int len) {
   return ((i + 1 < len) ? i + 1 : 0);
}

/**
  * Decrement i modulo len.
  */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

4、ThreadLocalMap为什么要使用弱引用

我们知道 ThreadLocalMap 中的 key 是弱引用,而 value 是强引用才会导致内存泄露的问题,至于为什么要这样设计,这样分为两种情况来讨论:

①、key 使用强引用:这样会导致一个问题,引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,则会导致内存泄漏。

②、key 使用弱引用:这样的话,引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。

比较以上两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候被清除,算是最优的解决方案。

5、ThreadLocal内存泄露问题以及如何解决

由之前的ThreadLocalMap类的内部类Entry的源码可知:通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

对于ThreadLocal内存泄露的另一种解释如下:
ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被 Entry 中的 Key 引用的,因此如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。这个时候 Entry 中的 key 已经被回收,但是 value 又是一强引用不会被垃圾收集器回收,这样 ThreadLocal 的线程如果一直持续运行,value 就一直得不到回收,这样就会发生内存泄露。

ThreadLocal内存泄露解决:
①、在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。如下所示:

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("小狼");
    // 其它业务逻辑
} finally {
    localName.remove();
}

②、其实,最好的方式就是将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,可以防止内存泄露。

推荐阅读:
Java面试必问:ThreadLocal终极篇
Java 200+ 面试题补充 ThreadLocal 模块
Java并发编程之ThreadLocal详解

你可能感兴趣的:(面试题)