ThreadLocal的使用原理及内存泄漏原因

ThreadLocal的使用原理及内存泄漏原因

ThreadLocal类,字面意思是本地变量,ThreadLocal可以为在每个线程中都创建一个副本,每个线程可以访问自己内部的副本变量。

1.API使用

简单看看如何使用这个类:

public class ThreadLocalTest {
    public static void main(String[] args) {
        ThreadLocal<String> name = new ThreadLocal<>();

        new Thread(()->{
            name.set("ABin");
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+": "+name.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"线程1").start();

        new Thread(()->{
            name.set("Someone");
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName()+": "+name.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"线程2").start();
    }
}

输出结果:
线程1: ABin
线程2: Someone

从结果可以看出,线程1和线程2都修改了name的值,但是无论我们运行多少次,都不会出现线程同步的问题,因为两个线程内都有一个name变量的副本,各自修改互不影响。

2.线程中副本变量如何实现的

从上面的例子可以看出,ThreadLocal通过setget方法,来设置、读取线程中的本地变量,那么先看这两个方法:

    public void set(T value) {
        Thread t = Thread.currentThread();		// 获取当前运行线程
        ThreadLocalMap map = getMap(t);		// 获得当前运行线程的ThreadLocalMap
        if (map != null)
            map.set(this, value);		// 如果不为null,则放入此map
        else
            createMap(t, value);		// 如果为空则为此线程创建map
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    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();
    }

从这段代码可以看出,每个线程在调用set方法的时候,都会向线程中的ThreadLocalMap变量中放入ThreadLocal的引用value。而每次调用get方法的时候,都是从线程中的ThreadLocalMap变量中取值。

那么看看ThreadLocalMap类做了什么:

// 这里只摘录了重要的代码,详细的可以去看源码
// 首先可以看到 ThreadLocalMap 是 ThreadLocal 的一个静态内部类
    static class ThreadLocalMap {
    // 这里使用了弱引用,作用就是当一个变量没有被强引用,且JVM发起GC的时候,会回收被弱引用的变量的空间
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        
        private Entry[] table;
        
// get()时会调用此方法,返回Map中对应ThreadLocal变量在当前线程中的值        
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);		// 如果不存在这个Entry,或者这个Entry的键是null,就会走这个方法。
        }

// 这个方法的作用:1.如果Entry是null,那么返回null,也就是不存在这个ThreadLocal变量。2.如果Entry的键为null,那么走expungeStaleEntry方法,清理掉所有键为null的Entry
// 什么时候Entry的键为null?没有变量强引用ThreadLocal变量,且JVM内存不够发起一次GC后
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            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;
        }
        
        // 这个方法会将键为空的Entry的value和Entry都置为null,方便GC
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

		// 向Map中存放一个ThreadLocal和value
        private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            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();
        }

		// 从map中移除一个ThreadLocal变量,同时会清理掉所有键为null的Entry
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
    }

总结一下:每个线程中都有一个ThreadLocalMap变量,这个变量可以存放多个ThreadLocal变量及其值,当某一个线程使用ThreadLocal实例的set()、get()时候,会向这个线程的Map中存、取。

3.为什么Entry使用弱引用

首先明确,Java中引用传递的是堆中的地址,那么如果ThreadLocal被置为null或指向其他实例的时候,JVM理应回收这部分空间(因为ThreadLocal实例改变了,原来内存中的值无法通过get()得到了)。但是如果Entry采用强引用,并且线程迟迟不结束(常见的是线程池),那么就会导致这部分空间明明没有用但无法清理,也就是内存泄漏产生的一个原因。

4.内存泄漏

虽然采用了虚引用,但还是存在内存泄漏的可能:Entry只虚引用了ThreadLocal,而value则是被Entry强引用的。如果ThreadLocal指向发生改变,GC的时候会回收ThreadLocal的空间,并不会回收value的空间,**可以通过remove()**来安全的移除。

// ThreadLocal
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
     
// ThreadLocalMap     
     private void remove(ThreadLocal<?> key) {
     	Entry[] tab = table;
         int len = tab.length;
         int i = key.threadLocalHashCode & (len-1);
         for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
         	if (e.get() == key) {
                 e.clear();		// 清除虚引用
                 expungeStaleEntry(i);	// 会将value置为null,方便GC
                 return;
         	}
         }
     }

所以JDK建议将ThreadLocal变量定义成private static的,这样ThreadLocal的生命周期更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

此外,上面也提到过ThreadLocalMap的getEntry()或者set(),会清理掉Map中键为null的记录。

5.为什么不把value设为虚引用

因为如果 value实例被释放了,但ThreadLocal中的value还有可能需要继续使用。

public class ThreadLocalTest {
    static ThreadLocal<String> name = new ThreadLocal<>();
    public static void main(String[] args) {
        new Thread(()->{
            name.set("ABin");   // value引用的字符串常量池中的值
        }).start();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值