ThreadLocal源码解析

Posted pinxiong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal源码解析相关的知识,希望对你有一定的参考价值。

在多线程的情况下,ThreadLocal提供了一个种为每个线程访问相同的变量,并且线程对变量的更新互不影响的机制。也是对象实现线程安全的一种方式。

ThreadLocal的实现机制

我们常用的方法有getsetinitialValue,这次将会围绕这几个方法的源码进行深入解析

  • get方法
    //  获取元素
    public T get() {
        //  当前线程
        Thread t = Thread.currentThread();
        //  通过当前线程获取ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //  获取Entry,其中key为ThreadLocal对象自身
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;        //  获取对象的值
                return result;
            }
        }
        //  返回initialValue的值
        return setInitialValue();
    }

首先,通过当前线程对象获取ThreadLocalMap对象,然后以ThreadLocal对象自身为key获取ThreadLocalMap.Entry,最后在获取Entry中的value

代码的逻辑非常简单,我们再来看看getMapmap.getEntry方法

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

threadLocals是Thread的一个属性

public class Thread implements Runnable {
    //  ......
    //  threadLocals是Thread的一个属性
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
  1. map.getEntry方法
    ThreadLocalMapThreadLocal对象的一个内部类,EntryThreadLocalMap的一个内部类
    //  ThreadLocal的内部类
    static class ThreadLocalMap {
        //  Entry是ThreadLocalMap的内部类,是一个弱引用对象
        static class Entry extends WeakReference<ThreadLocal<?>> {
            //  ThreadLocal中的value
            Object value;
            //  Entry的Key为ThreadLocal对象
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //  Entry数组,用来存放一个线程的多个ThreadLocal变量
        private Entry[] table;
        //  根据ThreadLocal来获取对应的value
        private Entry getEntry(ThreadLocal<?> key) {
            //  通过hash算法获取key在数组中对应的下标
            int i = key.threadLocalHashCode & (table.length - 1);
            //  获取下标对应的Entry对象
            Entry e = table[i];
            //  获取value
            if (e != null && e.get() == key)
                return e;
            else
                //  当key不存在时获取值,有2中可能
                //  1. 可能过期了
                //  2. 可能扩缩容
                return getEntryAfterMiss(key, i, e);
        }
}

key过期了如何获取值

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                //  如果key存在,直接返回value
                if (k == key)
                    return e;
                //  如果key为空说明已经过期了,需要清除
                if (k == null)
                    expungeStaleEntry(i);
                else
                    //  获取下一个key,看看能否找到
                    //  这是由于清除已经过期的key,
                    //  改变了Entry数组的size引起的位置变更
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
  • set方法
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

先获取ThreadLocalMap对象,然后在以ThreadLocalkey,将value设置到ThreadLocalMap对象中

  1. map.set方法
        //  将ThreadLocal对应的value存储到ThreadLocalMap对象中
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //  计算table中的下标
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                // 获取ThreadLocal对象 
                ThreadLocal<?> k = e.get();
                //  如果Entry数组中存在ThreadLocal对象,则替换之前的值
                if (k == key) {
                    e.value = value;
                    return;
                }
                //  去掉过期的ThreadLocal对象
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //  Entry数组中不存在ThreadLocal对象,创建一个新的Entry对象
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //  清除过期的对象
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                // Entry数组的大小改变以后重新计算hash 
                rehash();
        }
  1. createMap方法
void createMap(Thread t, T firstValue) {
        //  当线程的threadLocals为null时,为线程初始化一个ThreadLocalMap对象
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • initialValue方法
// 可以通过重写该方法来返回默认值
protected T initialValue() {
        return null;
    }

ThreadLocal内存泄漏问题

首先看一下ThreadLocal中对象的引用关系图

技术图片

从ThreadLocal中对象的引用关系来看,ThreadThreadLocalMapEntry对象之间都是强引用,如果可能出现内存泄漏那就是ThreadLocal对象弱引用引起的。

什么时候会发生内存泄漏

ThreadLocal实例不在有强引用指向,只有弱引用存在,且GC回收了这部分空间时,也就是Entry对象中的key被回收了,但是value还没有被回收,这时会出现内存泄漏,因为value无法得到释放。

如何避免内存泄漏

ThreadLocalMap中是通过expungeStaleEntrykeynull的对象对应的value也设置为null

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        //  如果key为null,会将value也设置成null
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                 tab[i] = null;
                while (tab[h] != null)
                      h = nextIndex(h, len);
               tab[h] = e;
            }
        }
    }
    return i;
}

所以只要调用expungeStaleEntry方法,且keynull时就可以回收掉value了,我们可以通过调用ThreadLocalremove方法进行释放

避免ThreadLocal出现内存泄漏的方式有

  1. 调用ThreadLocalremove方法
  2. ThreadLocal变量定义成static类型的,对ThreadLocal的强引用不会消失,所以也不存在内存泄漏的问题,但是可能会有所浪费

以上是关于ThreadLocal源码解析的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal源码解析-Java8

ThreadLocal源码解析

JDK1.8中ThreadLocal源码解析

JDK1.8中ThreadLocal源码解析

ThreadLocal源码解析

ThreadLocal源码解析