美文网首页
ThreadLocal 内部实现、应用场景和内存泄漏

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

作者: yuan_dongj | 来源:发表于2018-03-21 20:02 被阅读0次

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。

相关文章

网友评论

      本文标题:ThreadLocal 内部实现、应用场景和内存泄漏

      本文链接:https://www.haomeiwen.com/subject/jvmgqftx.html