ThreadLocal原理以及为什么会出现内存泄漏

                                             ThreadLocal原理以及为什么会出现内存泄漏

一、ThreadLocal

     ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

    ThreadLocal类主要有四个方法,分别是:

          1)ThreadLocal.get:用来获取ThreadLocal在当前线程中保存的变量副本

          2)ThreadLocal.set:用来设置ThreadLocal在当前线程中变量的副本

          3)ThreadLocal.remove:用来删除ThreadLocal在当前线程中变量的副本

         4)ThreadLocal.initialValue:是一个protected方法,一般是用来在使用时进行重写的。在调用get()方法时,如果ThreadLocal没有被当前线程赋值或当前线程刚调用remove方法,就返回此方法值。

1.ThreadLocal的使用

public class ThreadLocalTest {

    public static ThreadLocal<String> threadLocalVar = new ThreadLocal<String>() {
        @Override
        public String initialValue() {
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!");
            return "hello";
        }
    };

    public static void main(String[] args) {
        new Thread(new SimpleThread("beijing")).start();
        new Thread(new SimpleThread("北京")).start();
    }

}

class SimpleThread implements Runnable {
    private String value;

    public SimpleThread(String value) {
        this.value = value;
    }


    public void run() {
        Thread t = Thread.currentThread();
        String value1 = ThreadLocalTest.threadLocalVar.get();
        System.out.println("threadName=" + t.getName() + ";最初value1=" + value1);
        ThreadLocalTest.threadLocalVar.set(value);
        String value2 = ThreadLocalTest.threadLocalVar.get();
        System.out.println("threadName=" + t.getName() + ";设值后value2=" + value2);
        ThreadLocalTest.threadLocalVar.remove();
        String value3 = ThreadLocalTest.threadLocalVar.get();
        System.out.println("threadName=" + t.getName() + ";删除后value3=" + value3);
    }
}

运行结果:

Thread-0调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!
Thread-1调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!
threadName=Thread-0;最初value1=hello
threadName=Thread-0;设值后value2=beijing
threadName=Thread-1;最初value1=hello
threadName=Thread-1;设值后value2=北京
Thread-1调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!
threadName=Thread-1;删除后value3=hello
Thread-0调用get方法时,当前线程共享变量没有设置,调用initialValue获取默认值!
threadName=Thread-0;删除后value3=hello

二、ThreadLoal原理

1.ThreadLocal.get

/**
     * 返回该thread-local变量在当前线程中的值
     * 如果当前线程中没有该hread-local变量的值,则该变量值被初始化成调用initialValue()方法后返回的值
     * @return
     */
    public T get() {
        Thread t = Thread.currentThread();//返回当前正在执行的线程的引用
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);//注意此时的“this”是指该thread-local变量
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //当前线程中没有该thread-local变量的值
        return setInitialValue();
    }

   //返回该线程Thread内部的一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals
    ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
public class Thread implements Runnable {

        /* ThreadLocal values pertaining to this thread. This map is maintained
         * by the ThreadLocal class. */

        ThreadLocal.ThreadLocalMap threadLocals = null;

1)如果当前线程中没有该thread-local变量的值

  //设值初始值
    private T setInitialValue() {
        /**
         * initialValue方法默认情况下返回的是null
         * 注意如果不重写initialValue()方法,在get之前不调用set也能正常运行,不会出现空指针异常,
         * 只不过该thread-local变量在当前线程中的值为null而已
         */
        T value = initialValue();
        java.lang.Thread t = java.lang.Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);//注意此时的“this”是指该thread-local变量
        else
            createMap(t, value);
        return value;
    }

      

 i)如果ThreadLocal.ThreadLocalMap map = getMap(t);中 map为null

 void createMap(java.lang.Thread t, T firstValue) {
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);//注意此时的“this”是指该thread-local变量
    }
static class ThreadLocalMap {

            /**
             * The entries in this hash map extend WeakReference, using
             * its main ref field as the key (which is always a
             * ThreadLocal object).  Note that null keys (i.e. entry.get()
             * == null) mean that the key is no longer referenced, so the
             * entry can be expunged from table.  Such entries are referred to
             * as "stale entries" in the code that follows.
             */
            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;

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

            /**
             * 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) {
                table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }

  从代码中,可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。

 ii)如果ThreadLocal.ThreadLocalMap map = getMap(t);中 map不为null

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

        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);//计算index索引位置

        //从i位置开始,依次加1直至找到没有被其他key值的元素占用的位置或找到自己相同key的位置,并替换掉之前的值
        for (ThreadLocal.ThreadLocalMap.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 ThreadLocal.ThreadLocalMap.Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    从上面的代码可以看出:ThreadLocal中hash冲突的解决方法:

    和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

   ThreadLocalMap解决Hash冲突的方法就是简单的步长加1,寻找下一个相邻的位置。

2)如果当前线程中有该thread-local变量的值

 //以当前thread-local变量值作为key,从ThreadLocalMap中获取对应的Entry节点
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)//没有hash冲突,直接返回对应index位置上的值
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }

    //有hash冲突的情况下,以当前thread-local变量值作为key,从ThreadLocalMap中获取对应的Entry节点
    private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;

        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                return e;
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

  对get方法进行总结:

        在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

  初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

  然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

2.ThreadLocal.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);
    }

set方法中的代码,我们在get方法中已经分析过了

3.ThreadLocal.remove

   public void remove() {
        ThreadLocal.ThreadLocalMap m = getMap(java.lang.Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
    private void remove(ThreadLocal<?> key) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        //只所以用for循环查找key,是因为要考虑hash冲突的情况
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }
    public void clear() {
        this.referent = null;
    }

三、ThreadLocal为什么会出现内存泄漏

       ThreadLocal的核心机制:

      每个Thread内部维护着一个ThreadLocalMap,它是一个Map。这个映射表的Key是一个弱引用,其实就是ThreadLocal本身,Value是真正存的线程变量Object。

      也就是说ThreadLocal本身并不真正存储线程的变量值,它只是一个工具,用来维护Thread内部的Map,帮助存和取。所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

1.ThreadLocal为什么会出现内存泄漏

        static class ThreadLocalMap {
            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;

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

      ThreadLocal 本身并不存储值,它只是作为一个 key保存到ThreadLocalMap中,但是这里要注意的是它作为一个key用的是弱引用,因为没有强引用链,弱引用在GC的时候可能会被回收。这样就会在ThreadLocalMap中存在一些key为null的键值对(Entry)。因为key变成null了,我们是没法访问这些Entry的,但是这些Entry本身是不会被清除的,为什么呢?因为存在一条强引用链。即线程本身->ThreadLocalMap->Entry也就是说,恰恰我们在使用线程池的时候,线程使用完了是会放回到线程池循环使用的。由于ThreadLocalMap的生命周期和线程一样长,如果没有手动删除对应key就会导致这块内存即不会回收也无法访问,也就是内存泄漏。

   其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。但是这些举动不能保证内存就一定会回收,因为可能这条线程被放回到线程池里后再也没有使用,或者使用的时候没有调用其get(),set(),remove()方法。

  内存泄漏归根结底是由于ThreadLocalMap的生命周期跟Thread一样长

  1)如何避免内存泄漏

        调用ThreadLocal的get()、set()方法后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

      如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

      在不使用线程池的前提下,即使不调用remove方法,线程的"变量副本"也会被gc回收,即不会造成内存泄漏的情况。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值