深入理解ThreadLocal底层实现及其内存泄漏问题

1. ThreadLocalMap


  • Thread类中包含了一个这样的成员变量:

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

    也就是说,每一个Thread实例都有一个属于自己ThreadLocalMap,这个容器是在ThreadLocal类中定义,我们通过ThreadLocal类来操作这个容器。

  • 这个容器也就是我们所说的线程本地存储,对于其他的线程是不可见的

2. 什么是ThreadLocal


  • ThreadLocal是一个类,他是一个可以操作ThreadLocalMap的一个工具类。 也就是说,我们用ThreadLocal的实例来操作类型为ThreadLocalMap的Thread.threadlocals
  • 在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

3.ThreadLocal的功能


经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

4. 例子


public class ThreadLocalTest {
    static ThreadLocal<String> tl = new ThreadLocal<>();
    static ThreadLocal<String> tl1 = new ThreadLocal<>();

    public static void twoThreadLocal(){
        new Thread(()->{
            tl.set("hello");
            System.out.println(tl1.get());
        }).start();
    }
}

5. ThreadLocal底层实现

5.1 ThreadLocalMap的数据结构


  • ThreadLocalMap其实是一个hash map

  • 这个类内部维护了一个Entry数组。每一个Entry实例都是一个键值对: key是一个ThreadLocal对象,值是一个Object对象.

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

    我们通过这个Entry,和ThreadLocal的用法,就能判断:这个容器记录的是我们通过哪一个ThreadLocal实例存进来的数据,到时候该ThreadLocal实例调用get()方法时,也只返回通过这个实例存进来的数据。这也就是为什么上面的例子中tl1 get的是null

5.2 ThreadLocal.set()


public void set(T value) {
        Thread t = Thread.currentThread();
//调用getmap,将当前线程作为参数传入,这样就获取到了当前线程ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {//如果这个map不是空的就之间向里添加
            map.set(this, value);//将this,也就是当前ThreadLocal实例,和要存入的value一同存入到map中
        } else {//如果是空的就创建一个
            createMap(t, value);
        }
    }

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

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

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

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

这个set非常简单:

  1. 首先 这个方法调用getmap(),将当前线程作为参数传入,这样就获取到了当前线程ThreadLocalMap

  2. 在判断当前线程的ThreadLocalMap是否为null

  3. 如果是null则创建一个;反之将this,也就是当前ThreadLocal实例,和要存入的value一同存入到map中

  4. ⚠️:观察 set(ThreadLocal<?> key, Object value) 我们可以知道,如果entry列表的某个位置enrty不为空,我们再向这个位置添加元素的话会被替换。

        static ThreadLocal<String> tl = new ThreadLocal<>();
        public static void setTest(){
            tl.set("hello");
            tl.set("hello1");
            tl.set("hello2");
            System.out.println(tl.get());//将只输出hello2
        }
    

这也就是之前说的,通过ThreadLocal来操作线程的本地存储,而这个本地存储就是用ThreadLocalMap来实现的。

5.2 ThreadLocal.get()


public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//这里的this就是当前调用方法的ThreadLocal实例
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

同样

  1. 首先,我们调用getMap(),获取到当前线程的ThreadLocalMap

  2. 之后调用getEntry(this),获取到一个key全是当前ThreadLocal实例的Entry。

5.3 Entry


weakreference的一个典型应用就是在ThreadLocal中。

  • Entry继承的是 WeakReference。
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
                super(k);//相当于new WeakReference(K)
                value = v;
            }
}
  • 当我们创建一个Entry对象时,调用了它的构造器,这个构造器里它调用了super,也就是WeakReference的构造器,也就是相当于new WeakReference(K).

  • 也就是说:Entry中的key是通过弱引用指向一个ThreadLocal

  • 引用“链”:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hfzuo1wr-1597103913155)(/Users/luca/MarkText-img-Support/弱引用在ThreadLocal中的应用.png)]

为什么Entry key 要用弱引用?


如果我们Entry不使用弱引用,就算当我们的tl = null时,这个key也不会被回收。而一些线程,比如跑在服务器上的线程一般都是不会结束的,24 hours a day ,7 days a week,所以这个map是长期存在的,如果这是使用强引用,则这个key也是长期存在的。所以会导致内存泄漏(Memory Leak).

如果使用弱引用,一旦我们把tl的强引用一撤,gc一来,则key就马上会被回收,为null。

为什么Entry的value不用弱引用


我们没有办法将value设置为弱引用,如果设置为弱引用的话,gc一来就被回收了,就没有用了,之所以key可以变为弱引用,是因为它还有另外一个强引用值向它

使用Threadlocal,当里面的对象不用时,务必手动remove


因为我们的value还是强引用,所以就算key指向空了,value还不是空,久而久之还是会出现内存泄漏, 所以### 使用Threadlocal,当里面的对象不使用时,务必remove!!!

ThreadLocal<M> tl = new ThreadLocal();
        tl.set(new M());
        tl.remove();
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值