面试官都爱问这个:ThreadLocal与内存泄漏

ThreadLocal是面试中经常问到的点,今天我们来讲解下ThreadLocal。

1、什么是ThreadLocal

Thread类有一个成员变量threadLocals:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

我们发现Thread类中并没有提供成员变量threadLocals的设置与访问的方法,那么每个线程的实例threadLocals参数我们如何操作呢?这时我们的主角:ThreadLocal就登场了。

ThreadLocal是线程Thread中属性threadLocals的管理者。也就是说我们对于ThreadLocal的get, set,remove的操作结果都是针对当前线程Thread实例的threadLocals存,取,删除操作。

ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:

1)存储单个线程上下文信息。比如存储id等;

2)减少参数传递。比如做一个trace工具,能够输出工程从开始到结束的整个一次处理过程中所有的信息,从而方便debug。由于需要在工程各处随时取用,可放入ThreadLocal。

2、ThreadLocal使用实例

为了更直观的体会ThreadLocal的使用我们假设如下场景:

1)我们给每个线程生成一个ID

2)一旦设置,线程生命周期内不可变化

3)容器活动期间不可以生成重复的ID

package com.testthreadlocal;

import java.util.concurrent.atomic.AtomicInteger;

public class Test {
	
	public static void main(String[] args)
    {
        // main Thread
        incrementSameThreadId();
        new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                incrementSameThreadId();
            }
        }).start();
        new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                incrementSameThreadId();
            }
        }).start();
    }
   
    private static void incrementSameThreadId()
    {
        try
        {
            for (int i = 0; i < 5; i++)
            {
                System.out.println(Thread.currentThread() + "_" + i + ",threadId:" + ThreadLocalId.get());
            }
        }
        finally
        {
            // 使用后请清除
            ThreadLocalId.remove();
        }
    }
}

/**
 * inner class.
 */
class ThreadLocalId
{
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>()
    {
        @Override
        protected Integer initialValue()
        {
            return nextId.getAndIncrement();
        }
    };

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get()
    {
        return threadId.get();
    }

    // remove currentid
    public static void remove()
    {
        threadId.remove();
    }
}

输出结果如下,不同线程间id不同,相同线程id相同。

Thread[main,5,main]_0,threadId:0
Thread[main,5,main]_1,threadId:0
Thread[main,5,main]_2,threadId:0
Thread[main,5,main]_3,threadId:0
Thread[main,5,main]_4,threadId:0
Thread[Thread-0,5,main]_0,threadId:1
Thread[Thread-0,5,main]_1,threadId:1
Thread[Thread-0,5,main]_2,threadId:1
Thread[Thread-0,5,main]_3,threadId:1
Thread[Thread-0,5,main]_4,threadId:1
Thread[Thread-1,5,main]_0,threadId:2
Thread[Thread-1,5,main]_1,threadId:2
Thread[Thread-1,5,main]_2,threadId:2
Thread[Thread-1,5,main]_3,threadId:2
Thread[Thread-1,5,main]_4,threadId:2

3、ThreadLocal源码解析

ThreadLocal类结构及方法解析:
在这里插入图片描述
ThreadLocal三个方法get, set , remove以及内部类ThreadLocalMap、Entry。

我们以get方法为例:
在这里插入图片描述
其中getMap(t)返回的是当前线程的threadLocals,如下图,然后根据当前ThreadLocal实例对象作为key获取ThreadLocalMap中的value,如果首次进来这调用setInitialValue()。
在这里插入图片描述
在这里插入图片描述
getEntry方法:当前ThreadLocal实例对象作为key,key的hashCode与table数组的长度做位与运算,得出table数组的索引下标,取出对应的Entry。
在这里插入图片描述
Entry类是ThreadLocalMap的静态内部类,继承了WeakReference,从它的构造方法可以看出,ThreadLocal k是一个弱引用,指向了当前ThreadLocal实例。
在这里插入图片描述
set方法类似,不再赘述。
在这里插入图片描述
梳理完源代码后,可以得到这样一个关系图:
在这里插入图片描述

4、ThreadLocal导致的内存泄漏

什么叫内存泄漏,对应的什么叫内存溢出?

① Memory overflow:内存溢出,没有足够的内存提供申请者使用。

② Memory leak:内存泄漏,程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。

显然是TreadLocal在不规范使用的情况下导致了内存没有释放。
在这里插入图片描述
Entry继承了WeakReference类,我们来回顾下引用的知识:
在这里插入图片描述
既然WeakReference在下一次gc即将被回收,那么我们的程序为什么没有出问题呢?我们测试下弱引用的回收机制:

import java.lang.ref.WeakReference;

public class Test {
	
	public static void main(String[] args) {
		String str = new String("hello world");
        WeakReference<String> weakReference = new WeakReference<String>(str);
        System.gc();

        if (weakReference.get() == null)
        {
            System.out.println("weakReference已经被GC回收");
        }
        else
        {
            System.out.println(weakReference.get());
        }

        while (true)
        {
        }
    }
}

输出结果:hello world,没有被回收,这是因为这个String对象有一个强引用和一个弱引用同时指向了它。

我们改下代码:

WeakReference<String> weakReference = new WeakReference<String>(new String("hello world"));

输出结果为:weakReference已经被GC回收,只有一个弱引用被回收了。

上面演示了弱引用的回收情况,下面我们看下ThreadLocal的弱引用回收情况。
在这里插入图片描述
如上图所示,我们在作为key的ThreadLocal对象没有外部强引用,下一次gc必将产生key值为null的数据,若线程没有及时结束必然出现,一条强引用链
Threadref–>Thread–>ThreadLocalMap–>Entry-> value,导致value对应的Object一直无法被回收,产生内存泄露。

查看源码我们可以看到,ThreadLocal的get、set和remove方法都实现了对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用了ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。

5、总结

使用ThreadLocal时会发生内存泄漏的前提条件:

① ThreadLocal引用被设置为null,且后面没有set,get,remove操作
② 线程一直运行,不停止。(线程池)
③ 触发了垃圾回收。(Minor GC或Full GC)

我们看到ThreadLocal出现内存泄漏条件还是很苛刻的,所以我们只要破坏其中一个条件就可以避免内存泄漏,但为了更好的避免这种情况的发生我们使用ThreadLocal时遵守以下两个原则:

① ThreadLocal申明为private static final。Private与final 尽可能不让他人修改变更引用, Static 表示为类属性,只有在程序结束才会被回收。

② ThreadLocal使用后务必调用remove方法,最简单有效的方法是使用后将其移除。

欢迎小伙伴们留言交流~~
浏览更多文章可关注微信公众号:diggkr

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页