ThreadLocal

ThreadLocal

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的。

应用场景

  • spring事务隔离级别的源码,Spring采用ThreadLocal的方式来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,管理多个事务配置之间的切换,挂起和恢复。主要是在TransactionSynchronizationManager。Spring的事务主要是ThreadLocal和AOP去做实现的
  • 多线程场景下日期不对,排查下来是SimpleDataFormat的锅,当时我们使用的是SimpleDataFormat的parse()方法,其内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
    当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。
    private static ThreadLocal<SimpleDateFormat> local =new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    static {
        SimpleDateFormat simpleDateFormat = local.get();
        String format = simpleDateFormat.format(new Date());
        System.out.println(format);
    }
  • 用户信息参考,方法之间相互调用使用用户信息很麻烦(1,通过接口传参,2,再次调用Feign)。所以使用ThreadLocal存放用户信息。

底层实现原理

好的面试官,我先说一下他的使用:

ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();

线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set值,注意这里提到remove之前。
首先看他的set源码:

public void set(T value) {
    Thread t = Thread.currentThread();// 获取当前线程
    ThreadLocalMap map = getMap(t);// 获取ThreadLocalMap对象
    if (map != null) // 校验对象是否为空
        map.set(this, value); // 不为空set
    else
        createMap(t, value); // 为空创建一个map对象
}

set源码很简单。主要是ThreadLocalMap关注一下,ThreadLocalMap是当前线程Thread一个叫threadLocals的变量中获取的。

ThreadLocalMap getMap(Thread t){
      return t.threadLocals;
}
public class Thread implements Runnable {
      ……

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

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

这里可以发现ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的ThreadLocals变量里面的,其他现成无法拿到从而实现了隔离。

ThreadLocalMap底层结构是怎么样子的呢

查看ThreadLocalMap接口发现其并未实现map接口 ,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap的next,所以不存在链表。

static class ThreadLocalMap {

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

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

结构大概长这样:


image.png

为什么需要数组呢?没有链表怎么解决Hash冲突呢?

用数组是因为开发过程中一个线程可能有多个ThreadLocal来存放不同类型的对象,他们都将放到你当前线程的ThreadLocalMap里,所以肯定要用数组来存。
至于Hash冲突,我们先看一下源码:

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

从源码中可以看到,ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个ThreadLocalHashCode,在插入过程中根据ThreadLocal对象的hash值,定位到table中的位置i。
然后判断一下:如果当前位置为空,就初始化一个Enrty对象放在位置i上。如果当前位置不为空,如果key相等那么就刷新value的值。如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。

对象存放在哪里。

在java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而对内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。

那么是不是说ThreadLocal的实例及其值存放在栈上呢

其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

共享现成的ThreadLocal数据怎么办?

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们的主线程创建了一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

private void test() {    
final ThreadLocal threadLocal = new InheritableThreadLocal();       
threadLocal.set("帅得一匹");    
Thread t = new Thread() {        
    @Override        
    public void run() {            
      super.run();            
      Log.i( "张三帅么 =" + threadLocal.get());        
    }    
  };          
  t.start(); 
}

Thread源码中,我们看看Thread.init初始化创建的时候做了什么:

public class Thread implements Runnable {
  ……
   if (inheritThreadLocals && parent.inheritableThreadLocals != null)
      this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  ……
}

我就截取了部分代码,如果线程的inheritThreadLocals变量不为空,且父线程InheritableThreadLocals也存在,那么就把父线程的InheritableThreadLocals给当前线程的InheritableThreadLocals。

内存泄漏

image.png

ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是Key和value都应该被外界强引用才对,但是现在key被设计称了WeakReference弱引用了。


image.png

key是ThreadLocal,value是用户自定的value值。

弱引用:只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。

怎么解决value造成的内存泄漏

在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。

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

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

那为什么ThreadLocalMap的key要设计成弱引用?

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

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容