吊打面试官之 ThreadLocal 详解


ThreadLocal 的基本原理

我们先看一下 ThreadLocal 的简单使用:

ThreadLocal<String> localName = new ThreadLocal();
localName.set("帅枫");
String name = localName.get();  // name = 帅枫
localName.remove();

从代码上看它的使用很简单,它可以实现线程间的数据隔离,一个线程使用 get()方法是不能拿到其他线程的值的。我们可以看一下 set()方法的源码:

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取 ThreadLocalMap 对象,可以知道线程里面有一个 ThreadLocalMap 类型的字段
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 不为空就这设置值
    	map.set(this, value);
    else
        // 为空就创建一个 map 对象
    	createMap(t, value);
}

set()源码很简单,我们看看里面的 ThreadLocalMap 是个什么东东:

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

我们知道获取的是当前线程中的一个 threadLocals 变量,那这又是个啥呢?

public class Thread implements Runnable {
    ……
        
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  
    ……
 }

我们可以看到,每个线程里面都有一个 threadLocals 变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程 Thread 的 threadLocals 变量里面的,别人没办法拿到,从而实现了隔离。

ThreadLocalMap 看起来像是一个 Map,其实不然,我们直接看源码:

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    ......
}

可以看到,里面还有一个内部类 Entry,是继承了弱引用了的,我们上面 set() 设置的值其实就是存储到了 Entry 里面的 value 这个字段了,其实ThreadLocalMap 里面的 set()方法里是一个 Entry 数组。但是我们发现 Entry 中并没有链表一样的指针,那他是怎么解决 Hash 冲突的呢?

我们看下 set()源码就知道了:

private void set(ThreadLocal<?> key, Object value) {

    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();
		// 如果不为空并且这个 Entry 对象的 key 正好是即将设置的 key,那么就直接更新 value
        if (k == key) {
            e.value = value;
            return;
        }
	    // 如果当前位置是空的,就初始化一个 Entry 对象放在该位置
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

解决 Hash 冲突除了拉链法还有一个是开放定址法,这里就是使用了开放定址法,如果该槽的位置存在元素了,就会顺着向后找,知道有一个是空的,这个 set()方法正式使用了这种方法。但是这个方法也是有缺陷的:

在使用 get()方法的时候,也会根据 ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的,相当于是线性级别的了。

下面看看 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
    	return getEntryAfterMiss(key, i, e);
}

这里我们简单的提一下:我们都知道线程都有一个自己的栈,它是线程私有的,而堆是线程公有的,由 ThreadLocal 的作用可知,那存储在 ThreadLocal 中的对象是不是存储在栈中呢?

其实不是的,它们都还是位于堆上的,只是通过一些技巧将可见性修改为了线程可见。

ThreadLocal 的内存泄露问题

为啥会存在内存泄露问题呢?

我们上面讲解了 Entry 是继承是弱引用的,并且里面的泛型就是 ThreadLocal 类型,ThreadLocal 在保存的时候会把自己当做 key 存在ThreadLocalMap中,正常情况应该是 key 和 value 都应该被外界强引用才对,但是现在key 被设计成 WeakReference 弱引用了。

我们先简单的介绍一些弱引用:

被弱引用的对象,不管内存是否足够,只要执行垃圾回收,就会回收弱引用对象。

这样,如果发生垃圾回收,并且创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,从而发生内存泄露。那怎么解决呢?

在代码的最后使用 remove 就好了,就像下面这样:

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("帅枫");
    ……
} finally {
    localName.remove();
}

remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。

既然使用弱引用会导致内存泄露,那么为什么 ThreadLocalMap 的 key 要设计成弱引用?

key 不设置成弱引用的话就会造成和 entry 中 value 一样内存泄漏的场景。

ThreadLocal 的应用场景

首先就是 Spring 中的事务隔离级别使用了 ThreadLocal,Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

如果我们想在一次请求中保持用户的登录信息,我们也是可以使用 ThreadLocal 的。

引用:

https://mp.weixin.qq.com/s/LzkZXPtLW2dqPoz3kh3pBQ


文章作者: Gtwff
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Gtwff !
  目录