ThreadLocal 内部实现、应用场景和内存泄漏

1. 写在前面

LZ原先对于ThreadLocal的了解,仅限于它内部是一个以当前线程为键的map,但查看源码发现键是ThreadLocal对象本身。今天终于彻底看了看它的内部原理,故写此文以便以后复习学习。

2. ThreadLocal相关类介绍

为了理解ThreadLocal类的工作原理,必须同时介绍与其工作甚密的其他几个类与方法。
ThreadLocal层次大纲.png
  • ThreadLocalMap(内部类,存储value)
    ThreadLocalMap中关于entry的定义:
    ThreadLocalMap.Entry.png
    从中可以发现Map的key是ThreadLocal,值是用户的值,并不是原先认为的以当前线程为键。值存在Entry内,而键存在了WeakReference内,WeakReference为弱引用对象,这里与ThreadLocal内存泄漏有一定关系。
  • Thread(使用ThreadLocalMap)

    在Thread有一行代码:
    Thread.png
    ThreadLocalMap定义在ThreadLocal中,引用是在Thread中。
  • set(),get(),getMap(),createMap()
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

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

    ThreadLocalMap getMap(Thread t) {
        // ThreadLocalMap在当前线程被所有ThreadLocal共享
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        // 初始化map,构建table与Enrty
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

看到这里,可以明白大致的流程了,小小的总结一下:

  1. ThreadLocalMap用来存储用户的value,这个map的引用在Thread类里,是全线程唯一的。
  2. 当set时,首先获取全线程唯一的ThreadLocalMap,key是ThreadLocal对象,get时类似。
  3. ThreadLocal不是用来解决多线程并发访问异常的,因为每一个线程的ThreadLocalMap都不是同一个;并且如果向ThreadLocal存入同一个对象,还是会存在并发访问异常,下面给出一个例子
public class Son implements Cloneable {
    public static void main(String[] args) {
        final Son p = new Son();
        Thread t = new Thread(new Runnable() {
            public void run() {
                ThreadLocal<Son> threadLocal = new ThreadLocal<Son>();
                System.out.println(threadLocal);
                threadLocal.set(p);
                System.err.println("克隆前: " + threadLocal.get());
                threadLocal.remove();
                try {
                    threadLocal.set((Son) p.clone());
                    System.err.println("克隆后: " + threadLocal.get());
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadLocal);
            }
        });
        t.start();
    }
}

输出结果.png
就是说,ThreadLocal对于共享对象,不同线程进行get时拿到的还是同一个对象,还是有并发访问问题,所以要在保存到ThreadLocal之前,通过克隆或者new来创建新的对象,然后再进行保存。
所以,ThreadLocal的作用是在同一个线程周期内,将变量在不同的方法或者模块中进行数据传递,或者通过放入clone对象,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

3. 每一个ThreadLocal对象是如何区分的

查看源码,可以看到

    //java提供的,可以用原子方式更新的 int值的类。
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private final int threadLocalHashCode = nextHashCode();

    private static int nextHashCode() {
        //原子性加一
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

对于每一个ThreadLocal对象,都有一个final的int值threadLocalHashCode;AtomicInteger 是static修饰的,全局唯一,每一次加一之后的值仍然可用,并且保证原子性。所以,每一个线程的ThreadLocal对象都有唯一的threadLocalHashCode值。

4. 为什么不使用当前线程作为key?

上面知道,每一个线程周期,都有一个全线程唯一的map用于存储value,如果线程内多个ThreadLocal对象set了value,那么以当前线程作为键是不能保证key的唯一性的;而每一个ThreadLocal对象都可以由threadLocalHashCode进行唯一区分,所以key使用为ThreadLocal方便存取。

5. ThreadLocal的内存泄露问题

通过上面的Entry源码,发现ThreadLocal的键是弱引用,下图中实现是强引用,虚线是弱引用。
ThreadLocal引用关系.png

如果ThreaLocal对象没有一个强引用,那么当gc时,ThreadLocal对象会被回收,ThreadLocalMap内Entry的key就变成null,但是enrty本身还是有一个强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,如果线程迟迟没有死亡,那么永远无法回收,造成内存泄漏。

ThreadLocalMap设计时的对上面问题的对策:
getEntry方法.png
ThreadLocalMap的getEntry函数的流程大概为:
  1. 首先从ThreadLocal的找到索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
  2. 如果e为null或者key不一致则向数组table的下一个位置查询,如果发现相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。

但是这些操作的前提是调用set方法或者getEntry方法,所以JAVA官方推荐将ThreadLocal定义为static全局唯一,避免丢失ThreadLocal强引用,就能保证随时remove调entry内的key与value。

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

推荐阅读更多精彩内容

  • 假期就这么溜走了,回想起来,如同梦幻一般,在天堂走了一遭,醒来后,还是冰冷的现实。那么,就在冰冷中清醒吧。
    小小筑阅读 298评论 0 0
  • 摘要:园林植物配置,又称植物造景或景观种植。园林植物配置不仅要遵循科学性,而且要讲究艺术性,力求科学合理的配置,创...
    广州龙康雕塑阅读 914评论 0 2
  • 今天到了一堆硬货,铳梦白菜 运气大大的 个人觉得比较值,买了这个尖端单行本可以出了哈
    马甲_季姬击鸡急阅读 469评论 1 3
  • 韶华已逝暗黄昏,烛火映照遮流年。 细品涓涓梦中见,不知踟蹰人世间。 门庭若市只为颜,真情假意难分辨。 唯有子兰心不...
    开小差的泰迪阅读 337评论 3 1