ThreadLocal的使用及内存泄漏剖析


前言


一、什么是ThreadLocal

  • 变量值的共享可以使用public static的形式,所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量该如何实现呢,就会用到ThreadLocal。
  • Threadlocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。
  • ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

二、内部实现

1.set方法

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //在ThreadLocalMap中拿到该线程数据存储 用线程ID作为Map的key
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    }

	void createMap(Thread t, T firstValue) {
	//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
     t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    

ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。

ThreadLocalMap构造方法

  /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        	//内部成员变量 INITIAL_CAPACITY初始容量是16
            table = new Entry[INITIAL_CAPACITY];
            //位运算,计算出需要存放的位置	
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。
前面讲过每个线程Thread持有一个ThreadLocalMap类型的实例threadLocals,结合此处的构造方法可以理解成每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。

ThreadLocalMap中set方法

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        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;
            //这里可以看出同一个Thread中都是共用的一个ThreadLocalMap的成员变量table
            //只是取得hash值不同
            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();
        }

2.引用结构图及内存泄漏原因

在这里插入图片描述
ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。
也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
如何解决内存泄漏
调用remove()方法,清除数据。

总结

  1. 对于某一ThreadLocal来讲,他的索引值i(线程id)是确定的,在不同线程之间访问时访问的是不同的Entry[]数组的同一位置即都为Entry[][i],只不过这个不同线程之间的Entry[]是独立的。
  2. 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个Entry[]数组,然后每个ThreadLocal实例在Entry[]中的索引i是不同的。
  3. ThreadLocalMap类是ThreadLocal的静态内部类。每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。这样的设计主要有以下几点优势:
    (1)这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能;
    (2)当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。
  4. 在每个线程中,都维护了一个threadlocals对象,在没有ThreadLocal变量的时候是null的。一旦在ThreadLocal的createMap函数中初始化之后,这个threadlocals就初始化了。以后每次ThreadLocal对象想要访问变量的时候,比如set函数和get函数,都是先通过getMap(Thread t)函数,先将线程的map取出,然后再从这个在线程(Thread)中维护的map中取出数据或者存入对应数据。
  5. 大白话理解:每个线程都有线程id,ThreadLocal根据线程id找到ThreadLocal的静态内部类,ThreadLocalMap类,这个ThreadLocalMap类同时也是Thread中ThreadLocal成员的一个属性。如果找不到,则构造ThreadLocalMap类,把里面的成员变量table(Entry[]),赋值,初始容量16。然后用ThreadLocal对象按位计算Hash值。把Entry对象(k:ThreadLocal对象,v:值),存进去。所以get的时候,直接用Thread中的ThreadLocal对象计算hash,从ThreadLocal对象的table中,把Entry取出来,再取value值。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值