ThreadLocal源码分析理解弱引用和内存泄漏

Posted lllllLiangjia

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal源码分析理解弱引用和内存泄漏相关的知识,希望对你有一定的参考价值。

目录

一、说明

三个API

二、源码分析

set

ThreadLocalMap底层结构

结构图

map.set

get

remove

expungeStaleEntry

resize扩容

三、思考与总结

想共享线程的ThreadLocal数据怎么办?

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

示例代码

代码中对象所在堆栈的对应图

释放线程中引用后堆栈对应图

使用弱引用将key为null,对应的Entry如何清除?

内存泄露问题

解决


一、说明

ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,适用于各个线程不共享变量值的操作。

cookie,session等数据隔离都是通过ThreadLocal去做实现的

三个API

void set(T value):将此线程局部变量的当前线程副本设置为指定值。

T get():返回此线程局部变量的当前线程副本中的值。

void remove():删除此线程局部变量的当前线程值。

二、源码分析

set

得到当前线程,根据当前线程获取到ThreadLocalMap(ThreadLocal的内部类)集合,判断该集合是否为null,如果为null那就创建这个Map,如果不为null,那就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对象
}

由这个getMap方法可以看出,ThreadLocalMap类型的threadLocals是线程Thread中的一个属性,那Thread中包含ThreadLocalMap的实例对象时,Thread有指向TheadLocalMap的引用。这点先记住,后面会用。

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

ThreadLocalMap底层结构

    static class ThreadLocalMap {
		private Entry[] table;
        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(弱引用)的,每个线程对应一个ThreadLocalMap数组,但是每个线程中可能存在多个ThreadLocal类型的变量,这些ThreadLocal变量为Map的key值,变量存放的数据为Value。ThreadLocalMap数组初始化长度为16,对应16个Entry节点,每个ThreadLocal类型的变量对应一个节点。

结构图

ThreadLocalMap是一个Entry数组


image.png

map.set

用hashcode得到散列值获取到应该放在的数组索引

  • 如果该索引有值,并且key与要插入的值相等,那么就覆盖原来的旧value值。
  • 如果该索引有值,并且key等于null,说明旧Entry对应key的ThreadLocal变量已经被清空回收(此处有疑问,最后会解答),那这个Entry已经没有意义。新添加的key、value可以将这个旧的Entry替换掉。
  • 如果key值不等也不为null,此时发生hash冲突,那么遍历数组的下一个索引,直到为null为止跳出for循环,根据key和value初始化一个Entry对象放在tab数组对应的索引上。
    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];
                 // 不为空,for循环进来
                 e != null;
                 // 如果位置i的不为空,而且引用地址也不等,那就找下一个空位置,直到为空为止
                 e = tab[i = nextIndex(i, len)]) {
                // 指向的引用地址
                ThreadLocal<?> k = e.get();
                
                // 引用地址相等,直接替换返回
                if (k == key) {
                    e.value = value;
                    return;
                }
                // GC回收弱引用的对象key,此时这个Entry的key为null,但是value还有值
                if (k == null) {
                    // 在该索引位置,用新的key、value替换旧的Entry值
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // 如果tab[i]的Entry等于null,初始化一个Entry对象
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

get

  • 在get()方法中调用getEntry(this)方法。
  • 判断entry数组不为空,并且散列值得到的数组索引下的Entry的key等于要查询的key值,那么直接返回。
  • 如果两个key不相等,那么说明插入的时候发生了hash冲突。进入getEntryAfterMiss方法,while循环遍历数组。
    • 如果Entry的key和要查询的key值相等,那么返回该Entry
    • 如果遍历时Entry的key为null,那清除key为null的Entry,并且重新散列发生哈希冲突的Entry(此处有疑问,最后会解答)。
    • 遍历完之后如果没有相等值,那就返回null。
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 从ThreadLocalMap集合中查找
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //entry数组不为空并且散列值得到的数组索引下的Entry的key等于要查询的key值
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等  if (e != null && e.get() == key)。
            while (e != null) {
                ThreadLocal<?> k = e.get();
                // 相等就直接返回,不相等就继续查找,找到相等位置。
                if (k == key)
                    return e;
                // key为null,说明被GC回收了对应的ThreadLocal变量
                if (k == null)
                    // 清除key为null的Entry,并且重新散列发生哈希冲突的Entry
                    expungeStaleEntry(i);
                else
                    //如果key不一致,就判断下一个位置,如果set的时候哈希冲突严重的话,效率还是很低的。
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

remove

  • 调用remove方法,获取到本线程对应的ThreadLocalMap对象,如果不为null,则调用ThreadLocalMap内部类的方法remove(ThreadLocal<?> key)。
  • 根据哈希值获取对应的索引对应的entry,由于可能有哈希冲突的存在还要判断key是否相等
    • 如果key不相等那么向下遍历数组。
    • 如果key相等,那将entry对应的引用置为null,方便GC回收,并调用expungeStaleEntry方法,清除key为null的Entry,并且重新散列发生哈希冲突的Entry
     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)]) {
                // 根据哈希值获取对应的索引对应的entry,由于可能有哈希冲突的存在还要判断key是否相等
                if (e.get() == key) {
                    // entry对应的引用置为null,方便GC回收
                    e.clear();
                    // 清除key为null的Entry,并且重新散列发生哈希冲突的Entry
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

expungeStaleEntry

该方法做三件事

  1. 清空staleSlot对应索引的entry。
  2. 如果该索引后面entry的key为null,那么就进行清空。
  3. 如果该索引后面entry的key不为null,那就重新哈希。方便之后的get操作。
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // 将旧的entry以及对应的value置为null,方便回收
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            // 集合中的数量减一
            size--;

            // 重新哈希直到遇到 null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                // 将key为null的Entry值给清除
                if (k == null) {
                    // 将旧的entry以及对应的value置为null,方便回收
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    // 因为前面已经清空了一些旧的entry,前面的空位增多,
                    // 那些由于哈希冲突往后放置的entry可以重新散列,就可以往前放,这样get操作时遍历次数会减少
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

resize扩容

*2扩容

int newLen = oldLen * 2;

三、思考与总结

想共享线程的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(); 
}

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

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

每一个Thread都包含一个ThreadLocalMap,ThreadLocalMap可以包含多个Entry,Entry的key是不同的ThreadLocal变量。

Thread->ThreadLocalMap->ThreadLocal

这样的话,虽然ThreadLocal在客户端代码中置为null,那只是将各个线程栈指向堆中ThreadLocal对象的引用删除。但是作为key的ThreadLocal,在ThreadLocalMap里指向它的引用还存在,所以需要用弱引用,在没有其他强引用指向的时候,通过GC回收来删除key。

如果此时还困惑,借用知乎一篇博客(为什么Thread作为key用弱引用--图文解释)进行解释。

示例代码

public class ThreadLocalDemo {

   public static ThreadLocal<String> threadLocal =  new ThreadLocal<String>();

    public static void main(String[] args) {
        ThreadLocalDemo.threadLocal.set("hello world main");
        System.out.println("创建新线程前,主线程" + Thread.currentThread().getName() + "的threadlocal字符值为:"  + ThreadLocalDemo.threadLocal.get());

        try {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    ThreadLocalDemo.threadLocal.set("new thread");
                    System.out.println("新线程" + Thread.currentThread().getName() + "的threadlocal字符值为:" + ThreadLocalDemo.threadLocal.get());
                }
            };
            thread.start();
            thread.join();
        } catch (Exception e) {
            System.out.println(e);
        }
        System.out.println("创建新线程后,主线程" + Thread.currentThread().getName() + "的threadlocal字符值为:"  + ThreadLocalDemo.threadLocal.get());

    }
}

代码中对象所在堆栈的对应图

ThreadLocal对象的引用有两种

  1. main方法和thread的run方法中都有对ThreadLocal对象的直接引用,所以主方法栈和thread线程栈有引用(强引用)指向。
  2. 前面讲set方法时也提到了,每个线程对象中有ThreadLocalMap属性,ThreadLocalMap集合存储的Entry的key也是ThreadLocal变量,此时主线程对象和thread线程对象各自的ThreadLocalMap也都有引用(弱引用)指向。

image.png

当我们主动释放main线程栈和thread线程栈中的threadLocal引用,那此时堆栈图如下所示。

ThreadLocalDemo.threadLocal = null;

释放线程中引用后堆栈对应图

此时主方法栈和thread线程强引用都消失,只剩下弱引用。弱引用在GC回收的时候进行清除,所以此时ThreadLocalMap中该Entry的key为null,这就是为什么前面会出key为null的原因

image

使用弱引用将key为null,对应的Entry如何清除?

set()方法中当发现key为null的Entry,会调用replaceStaleEntry()方法,该方法可以替换掉陈旧的Entry元素

get()方法中当发现key为null的Entry,会调用expungeStaleEntry()方法,该方法可以删除陈旧的Entry元素

remove()方法中也会调用expungeStaleEntry()方法。

内存泄露问题

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

ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。

解决

在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。Entry就没有被指向的引用了

以上是关于ThreadLocal源码分析理解弱引用和内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal源码分析理解弱引用和内存泄漏

分析ThreadLocal的弱引用与内存泄漏问题

ThreadLocal 源码分析

深入浅出多线程编程实战ThreadLocal详解(内存泄漏)

深入浅出多线程编程实战ThreadLocal详解(内存泄漏)

ThreadLocal弱引用与内存泄漏分析