序言

ThreadLocal在日常开发中还是比较常见的,本文将从源码的角度彻底揭秘ThreadLocal,并会分享一些较为常见的面试题,let's go。

ThreadLocal是什么?

ThreadLocal隶属于lang包,它的主要功能是为每个线程提供一个私有的局部变量,这个变量在线程间相互隔离,互不影响。

主要解决的就是单例情况下全局变量的线程安全问题

ThreadLocal的底层实现

set方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
复制代码
  • 通过set方法可得知,先获取到当前的Thread对象,然后调用getMap(t)方法
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
 }
复制代码
  • getMap方法的内部实现也很简单,直接调用t的threadlocals字段,来获取到当前线程对应的ThreadLocalMap对象
  • 接下来会判断map是否存在,不存在的话就去创建出map,存在的话就调用map.set(this,value)方法了
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();

                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();
     }
复制代码
  • 以当前的ThreadLocal作为key,set的值作为value,然后封装成entry对象放到ThreadLocalMap当中。

当发生hash冲突时,采用的解决方式是线性探测法来解决的。

set方法小总结

通过set方法的阅读,我们基本可以得出以下结论:ThreadLocal本身不存放数据,而是通过Thread对应的ThreadLocalMap来存放数据,ThreadLocal只是作为key。

ThreadLocalMap

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
}
复制代码
  • ThreadLocalMap中通过一个内部类Entry来存放key,value,我们需要注意的是这个Entry对象是继承自WeakReference,WeakReference对象是一个弱引用对象(弱引用对象的特点是:当垃圾收集器进行gc时,如果没有引用指向弱引用对象的话,那么就会进行回收)

弱引用仅限于Key,value还是强引用对象

为什么key要设为弱引用?

我个人认为,key设为弱引用,是为了方便当ThreadLocal对象使用完毕后将key进行垃圾回收,避免出现内存泄漏。

key是不内存泄漏了,但是value还是会出现内存泄漏。(过会儿我们仔细说一下value的内存泄漏问题)

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();
    }
复制代码
  • get方法的主要作用就是获取我们set进去的值,先获取到threadLocalMap对象,然后将当前的threadLocal对象引用作为key从threadLocalMap中获取到对应的value。

从源码中可知,我们需要注意的是,有可能在操作threadLocal对象时,没有先执行set()方法,直接调用get()方法,那么它会返回setInitialValue()方法,我们一起来看看setInitialValue()做了什么。

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
复制代码
  • 第一行调用了initialValue()方法,我们先按下表,看看initialValue()做了什么
protected T initialValue() {
        return null;
    }
复制代码

initialValue()方法直接返回了null

  • 回到setInitialValue()方法,我们可以知道第一行代码T value = initialValue()执行完后,value是null,接下来的代码相信大家已经不陌生了
  • 继续获取到当前Thread对象,然后获取到ThreadLocalMap对象,然后以ThreadLocal对象作为key,null作为value写入到ThreadLocalMap当中。
  • 然后返回null

get方法小总结

会以当前的ThreadLocal作为key,从ThreadLocalMap中获取到set的value。

如果我们没有调用set,而直接调用get的话,默认情况下会返回null(并帮我们调用set方法,value就设为null)

initialValue方法

从上面我们可以知道,默认情况下initialValue方法是返回null的,其实我们在新建ThreadLocal对象时可以重写initialValue方法。

ThreadLocal<String> threadLocal=new ThreadLocal<String>(){
           @Override
           protected String initialValue() {
              return "haha";
          }
   };
  System.out.println(threadLocal.get());
复制代码

我们重写了initialValue方法,这样在直接get时就会获取到我们写的“haha”了,运行结果如下:

 

remove方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
         m.remove(this);
  }
  private void remove(ThreadLocal<?> key) {
      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)]) {
             if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
             }
        }
  }
复制代码

remove方法一般在用完ThreadLocal后进行调用,它的主要作用就是清除掉当前ThreadLocal对象在ThreadLocalMap中对应的entry。

ThreadLocal底层图

 

ThreadLocal的内存泄漏问题

内存泄漏:内存泄漏就是指我们使用完毕的资源,没有得到及时的释放,jvm还认为该资源有用,不会对其进行回收,导致该资源一直占用着我们的内存,最终很有可能导致内存溢出。

ThreadLocal的内存泄漏:上面我们提到ThreadLocalMap的key设为弱引用是为了解决key内存泄漏的问题,但value依旧是会有内存泄漏问题存在的。

当我们使用完ThreadLocal后,垃圾回收器会将key给回收掉,但是value却是一直存在的,直到线程结束才会释放,但我们日常开发中会有使用线程池的场景,在这个场景下线程的生命周期都是较长的,这个时间段内就造成了value的内存泄漏,因此ThreadLocal的内存泄漏和key是不是弱引用关系不大,主要还是由于使用完后没有调用remove()方法造成的。

为了避免内存泄漏,我们最好在使用完ThreadLocal后,调用其remove()方法。

ThreadLocal在Spring中的应用

Spring框架相信大家都不陌生,Spring框架中有一个@Transactional注解,它是用于保证事务的。

事务的主要作用就是保证同一事务下的操作要么全部成功,要么全部失败,但有一个前提条件就是这些操作必须使用同一个数据库连接,但是数据库连接不是线程安全的,它在多线程环境下会出现问题。

Spring为了保证事务的原子性,它就采用了ThreadLocal这个数据结构,用ThreadLocal来保存连接,set的类型是一个Map,key是数据源、value是连接,定义成map是为了应对多数据源的场景的,当采用了ThreadLocal后也就可以保证了我们同一线程在事务内的所有操作获取到的连接是同一个连接,也就保证了事务的原子性了。

原文链接:https://juejin.cn/post/6931159589203214350

如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份Java核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!