ThreadLocal源码学习笔记

前言

首先先对ThreadLocalz做一个简单介绍,接着学习一下set,get,remove方法,接着简单分析一下为什么可能会存在内内存泄漏。

ThreadLocal是什么

ThreadLocal是一个以ThreadLoacl自身为键,任意对象为值的存储数据结构,它是附带在线程上的。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。

ThreadLocal的数据结构

ThreadLocal源码学习笔记

图片来源https://www.jianshu.com/p/98b68c97df9b

  • 从图中我们可以看出,每个Thread都有自己的ThreadLocalMap,可以存储很多Entry对象。
  • ThreadLocalMap中的键值对为ThreadLocal和需要存储的对象。
  • 但是,Thread中的ThreadLocalMap由ThreadLocal维护。
  • 注意看图,一个ThreadLocal对象只能存储一个Entry,一个Entry中存储一对键值对ThreadLocal和value,如果需要存储多个对象变量,那么需要创建多个ThreadLocal对象进行set操作。

实现原理

  • ThreadLocal如何保存各自线程的对象变量呢?通过源码可以看到,在ThreadLocal中有一个静态内部类,也就是前面提到的ThreadLocalMap,而Thread类中有ThreadLocalMap属性,默认为null,在每个ThreadLocalMap中如上图所示,有多个Entry,在每个Entry中存储了想要存储的对象变量。由于ThreadLocalMap对象是Thread类的自带属性,是和每个线程绑定在一起的,这就是线程隔离的原因。
  • void set(Object value)在存储变量的时候,使用set方法将需要存储的对象变量存入到ThreadLocalMap中。注意,一个ThreadLocal对象只能存入一个对象变量。
  • public Object get()在取出值的时候,使用get方法取出存储的值。
  • public void remove()将当前存储的对象变量删除,减少内存,由于ThreadLocalMap是Thread类的自带属性,所以不使用remove方法,在当前线程结束时,ThreadLocalMap对象也会被回收。

先看个例子:创建多个线程,通过多个线程并行地对ThreadLocal对象进行set、get操作,并将值进行打印。观察打印的值是否和存的值一致。可以看出,创建了一个ThreadLocal变量,可以并行的对多个线程进行set和get想要的值。

package thread;

/**
 * Created by Administrator on 2019\4\30 0030.
 */

public class ThreadLocalTest {
    private static ThreadLocal<String> threadLocal = new ThreadLocal();
    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread_ thread_ = new Thread_(threadLocal, i);
            new Thread(thread_).start();
        }

    }

}

class Thread_ implements Runnable {
    ThreadLocal threadLocal;
    int value;
    Thread_(ThreadLocal threadLocal,int value) {
        this.threadLocal = threadLocal;
        this.value = value;
    }
    @Override
    public void run() {
        threadLocal.set("线程ID:"+Thread.currentThread().getName()+"的值:"+value);
        System.out.println(threadLocal.get());
        threadLocal.remove();
    }
}
//输出结果:

//线程ID:Thread-0的值:0
//线程ID:Thread-1的值:1
//线程ID:Thread-2的值:2
//线程ID:Thread-4的值:4
//线程ID:Thread-5的值:5
//线程ID:Thread-6的值:6
//线程ID:Thread-3的值:3
//线程ID:Thread-7的值:7
//线程ID:Thread-9的值:9
//线程ID:Thread-8的值:8

set方法

public void set(T value) {
        // 得到当前线程对象
        Thread t = Thread.currentThread();
        // 通过当前线程得到ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果map为不为null
        if (map != null)
            // 直接将value存入map,这里this是ThreadLocal自身对象
            // 在创建的map中也存在hash冲突的情况,这里采用了开放地址法,详细可以看源码的这个方法
            map.set(this, value);
        else
            // 否则创建ThreadLocalMap,再将value存入map
            createMap(t, value);
    }
ThreadLocalMap getMap(Thread t) {
    // 返回threadLocals对象,此属性是也就是Thread类的自带属性,一个ThreadLocalMap
        return t.threadLocals;
    }

set方法总结一下:

  1. 获得当前线程
  2. 通过当前线程对象获得当前线程的ThreadLocalMap属性。
  3. 判断map是否为null,不为null则将value存入map,这里也涉及到hash冲突问题以及扩容问题,hash冲突问题采用开放地址法进行处理,详细实现请看源码的这个方法。若为null,创建map,将value存入map。

所以可以看出,其实ThreadLocal相当于一个工具类,实质是将数据存在了每个Thread的map属性中。

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方法总结一下:

通过当前线程拿到当前线程的map,如果map不为null,取出ThreadLocalMap的Entry,再取出值,如果map为null,那么通过setInitialValue进行返回,通过这个方法可以重写进行初始化,如果没有重写,返回null。

从下面的代码我们可以看到,Entry是ThreadLocalMap的静态内部类,它继承于虚引用,但是这里只能对ThreadLocal进行回收,那么value作为强引用,在只有ThreadLocal作为键值对的键被回收后,value没有被回收便会造成内存泄漏。

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

 

ThreadLocal源码学习笔记

图片来源下面的解释也来自这个链接:http://www.importnew.com/22039.html

ThreadLocal为什么会内存泄漏

这是因为ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal在下次jvm垃圾回收时被回收,这个时候就造成了Entry中的key已经为null,但是value仍然存在,但又无法通过键来找到value,因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

但这样也并不能保证ThreadLocal不会发生内存泄漏,例如:

  • 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
  • 分配使用了ThreadLocal又不再调用get()、set()、remove()方法,那么就会导致内存泄漏。

为什么使用弱引用?

从表面上看,发生内存泄漏,是因为Key使用了弱引用类型。但其实是因为整个Entry的key为null后,没有主动清除value导致。很多文章大多分析ThreadLocal使用了弱引用会导致内存泄漏,但为什么使用弱引用而不是强引用?

官方文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。

下面我们分两种情况讨论:

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
  • 比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。

总结

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

 

 

参考文献

[1]https://www.jianshu.com/p/a1cd61fa22da

[2]https://www.jianshu.com/p/56f64e3c1b6c

[3]https://www.jianshu.com/p/98b68c97df9b