JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!

内存泄漏和内存溢出:

内存泄漏是指程序在申请内存后,无法释放已申请的内存空间就会造成内存泄漏。一次内存泄漏似乎不会造成很大影响。但内存泄漏累积的效果就是内存溢出。

场景1:是否会造成内存溢出:

场景描述:线程池中只有一个线程。每一次线程启动,均会初始化一个threadLocal对象。

该场景便是使用ThreadLocal的反面教材,即使用ThreadLocal但未使用remove()方法清除。

public class ThreadLocalDemo {
    private static Logger logger= LoggerFactory.getLogger(WeakReferenceDemo.class);
    private ThreadLocal<String> threadLocal = new ThreadLocal<>();

    //开启一个线程池
    public static void testThreadLocalByPool(){
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        while (true) {
            executorService.execute(() -> {
                //每new出一个对象,那么便new出一个ThreadLocal
                ThreadLocalDemo demo = new ThreadLocalDemo();
                demo.testThreadLocal();
            });
        }
    }
    public  void testThreadLocal() {
        //ThreadLocal是方法级别的
        threadLocal.set("aaa");
        String s = threadLocal.get();
        logger.info("获取ThreadLocal的内容:"+s);
    }
    //测试ThreadLocal不会发生内存溢出
    public static void main(String[] args) {
        testThreadLocalByPool();
    }
}

结论:该代码最终也不会发生内存溢出。

实际上ThreadLocal仅仅会造成内存泄漏,若是存在大量线程的情况(蚂蚁咬死象),可能会造成内存溢出。

ThreadLocal的源码分析

ThreadLocal含义为线程本地变量。即每个线程中均存在一个ThreadLocal对象。

图1-ThreadLocal与线程的关系.png

1.1 ThreadLocalMap的set方法

  1. 使用ThreadLocal的set方法存入value,实际上是获取当前线程的ThreadLocalMap对象,将threadLocal对象作为一个key存入到map中。
//java.lang.ThreadLocal#set
public void set(T value) {  
    //获取当前线程
    Thread t = Thread.currentThread();  
    //获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);  
    if (map != null)  
        //将threadLocal作为key,和value存入到ThreadLocalMap中
        map.set(this, value);  
    else  
        createMap(t, value);  
}  
  1. 现在的思路就是调用map的set方法。

Map底层就是一个数组,HashMap使用数组+链表的结构,其实是使用哈希桶去解决哈希冲突问题。而ThreadLocal自己实现Map结构,采用线性探测法去解决哈希冲突,所以单纯的使用数组便可以实现Map结构。

map中元素是依靠hashCode去计算在数组的位置的。但是总会有一些元素它们并不相等,但是他们的hashCode相同,即在数组中的位置相同。

static class Entry extends WeakReference<ThreadLocal<?>> {  
    /** The value associated with this ThreadLocal. */  
    Object value;  
  
    Entry(ThreadLocal<?> k, Object v) {  
        super(k);  
        value = v;  
    }  
}  

数组中的对象为Entry对象,Entry对象有两个属性,一个是弱引用持有的key,一个是强引用持有的value。

当key只被弱引用持有,并且发送了GC,key就会被回收,所以在Entry[]中会存在一些失效节点。ThreadLocalMap考虑到了这种情况,每次set的过程中,都会清除失效节点。这种补救措施便可以使得用户未显式调用reomve方法时,线程中的失效entry在set操作时也会被清理掉。


  • 颜色相同的节点表示HashCode相同;
  • 里面的值若是相同,代表两个节点的key完全相同;
image.png
private void set(ThreadLocal<?> key, Object value) {  
 
    Entry[] tab = table;  
    int len = tab.length;  
     //通过key的HashCode计算在map中的下标位置;
    int i = key.threadLocalHashCode & (len-1);  
    //情况1:下标位置存在元素。
    for (Entry e = tab[i];  
         e != null;  
         e = tab[i = nextIndex(i, len)]) {  
        ThreadLocal<?> k = e.get();  
        //若key相等,则去覆盖
        if (k == key) {  
            e.value = value;  
            return;  
        }  
        //若发现key为null,但是entry存在的节点,即已失效的节点
        if (k == null) {  
            //替换失效的节点。
            replaceStaleEntry(key, value, i);  
            return;  
        }  
    }  
    //若位置不存在元素,则生成entry对象,放入到map中。
    tab[i] = new Entry(key, value);  
    int sz = ++size;  
    if (!cleanSomeSlots(i, sz) && sz >= threshold)  
        rehash();  
}  
遇到失效节点.png
private void replaceStaleEntry(ThreadLocal<?> key, Object value,  
                               int staleSlot) {  
    Entry[] tab = table;  
    int len = tab.length;  
    Entry e;  
     //往前执行,寻找失效节点的范围
    int slotToExpunge = staleSlot;  
    for (int i = prevIndex(staleSlot, len);  
         (e = tab[i]) != null;  
         i = prevIndex(i, len))  
        if (e.get() == null)  
            slotToExpunge = i;  
   //若是在往后寻找的过程中,遇到key相等的节点,则与覆盖该节点并与失效节点交换
    for (int i = nextIndex(staleSlot, len);  
         (e = tab[i]) != null;  
         i = nextIndex(i, len)) {  
        ThreadLocal<?> k = e.get();  
       
        if (k == key) {  
            e.value = value;  
  
            tab[i] = tab[staleSlot];  
            tab[staleSlot] = e;  
  
            // 开始清除失效节点
            if (slotToExpunge == staleSlot)  
                slotToExpunge = i;  
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);  
            return;  
        }  
  
        if (k == null && slotToExpunge == staleSlot)  
            slotToExpunge = i;  
    }  
    //若是找到空节点,这将失效节点置空,并将值覆盖到失效节点上。
    tab[staleSlot].value = null;  
    tab[staleSlot] = new Entry(key, value);  
  
    if (slotToExpunge != staleSlot)  
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);  
}  

1.2 ThreadLocal的get方法

我们在线程中使用ThreadLocal.set()方法,是为了在线程运行到某处时调用ThreadLocal.get()方法获取到存储的值。

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();  
}  
private Entry getEntry(ThreadLocal<?> key) {  
    //通过HashCode计算出map中的位置
    int i = key.threadLocalHashCode & (table.length - 1);  
    Entry e = table[i];  
    //该位置的条目不为空,并且key相当则返回
    if (e != null && e.get() == key)  
        return e;  
    else  
        return getEntryAfterMiss(key, i, e);  
}  
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;  
}  

总结

ThreadLocal类提供的API方法实际上就是操纵当前线程对象中的ThreadLocalMap属性。ThreadLocalMap底层数据结构就是一个Entry数组对象。Entry有两个属性,key是弱引用持有的ThreadLocal对象,而value为我们存储的值。

在使用set或get方法时,会进行线性探测寻找对应的Entry对象,若发现失效节点,ThreadLocalMap会清除这些节点。但是也可以这样理解,若今后没有在此调用set或get方法,这些value永远不会被清除的。从而造成了内存泄漏。
当内存泄漏积少成多,最终可能会内存溢出。

2.1 ThreadLocalMap的key为什么设置为弱引用

ThreadLocalMap的key就是ThreadLocal对象,它在创建出来时,会被强引用和弱引用同时持有。当线程执行完任务后(伴随方法出栈),ThreadLoca只会被弱引用持有,一旦发生GC,key就会被置为null。而ThreadLocal的set或get操作,在线性探测定位entry时,遇到key==null的节点,会将其看做为失效节点进行回收。

2.2 ThreadLocal为什么是static修饰

优点:使用了static方法,实际上可以避免ThreadLocal对象重复创建;
缺点:使用了static方法,map的key就会被强引用持有,除非显式调用remove()方法,否则key不会被回收。

但是利大于弊。

在Spring的bean中使用ThreadLocal,因为bean大多数是单例,故ThreadLocal有无static修饰效果相同。

2.3 ThreadLocalMap的value为什么不会被回收

该value对象被强引用所持有。所以不会被回收。

2.4 ThreadLocal为什么会造成内存泄漏

TheadLocal是操作当前线程的ThreadLocalMap属性,该map的底层数据结构是Entry数组,Entry的value会强引用着对象。所以该对象不会被回收,造成内存泄漏。

相关阅读

JAVA并发(1)—java对象布局
JAVA并发(2)—PV机制与monitor(管程)机制
JAVA并发(3)—线程运行时发生GC,会回收ThreadLocal弱引用的key吗?
JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!
JAVA并发(5)— 多线程顺序的打印出A,B,C(线程间的协作)
JAVA并发(6)— AQS源码解析(独占锁-加锁过程)
JAVA并发(7)—AQS源码解析(独占锁-解锁过程)
JAVA并发(8)—AQS公平锁为什么会比非公平锁效率低(源码分析)
JAVA并发(9)— 共享锁的获取与释放
JAVA并发(10)—interrupt唤醒挂起线程
JAVA并发(11)—AQS源码Condition阻塞和唤醒
JAVA并发(12)— Lock实现生产者消费者

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