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实现生产者消费者

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354