Threadlocal never learn, and see which one will forget the (words summary)

Why learn ThreadLocal it? Because interviewers often ask, and use it to provide us with a local variable, local to a thread in the thread, so that you can reduce a thread because the operation between the multi-function leads to the complexity of the traditional values ​​of shared variables, it plainly we use ThreadLocal can be done anytime, anywhere access within a thread, and do not interfere with other threads.

In some special situations, the application ThreadLocal will bring great convenience, but many people do not understand Threadlocal but in the end is Gesha? In our interviews are also often asked Threadlocal, so based on our practical application and to deal with the interview, we have a need to learn good at Threadlocal.

Today, we have to finish Threadlocal under full account of learning, not to fight later learned, because after reading this article today, you're right Threadlocal forget it!

1. What is Threadlocal?

First of all, since we have to learn Threadlocal, then we need to know it's a what? From the point of view of our name, Threadlocal means that the thread local mean, we belong to this conjecture, not authority, then he is Gesha want to know, the best way is to look at the official definition of it is how we look at the ThreadLocal Source (based on jdk1.8) in the introduction to this class:

This class provides thread-local variables. These variables differ from* their normal counterparts in that each thread that accesses one (via its* {@code get} or {@code set} method) has its own, independently initialized* copy of the variable. {@code ThreadLocal} instances are typically private* static fields in classes that wish to associate state with a thread (e.g.,* a user ID or Transaction ID).

This is jdk1.8 in this class of ThreadLocal to comment, we simply Translated:

This class provides thread-local variables. These variables with normal variables, as each access a thread (through which {@code get} or {@code set} method) has its own thread, separate copy of the variable is initialized. {@code ThreadLocal} class instance is generally desirable state associated with the thread private static fields (e.g., user ID or transaction ID).

What does that mean? We can almost understand, he said TreadLocal can provide us with a local variable within a thread, and this variable and variable general also different, it is unique to each thread, and other threads interfere with each other.

Now we have a simple understanding of ThreadLocal, here we come directly on the code, see if it's a practical example.

2, How to use ThreadLocal?

Look at the code

First look at a piece of code:

public class Test {
    private static int a = 10;
    private static ThreadLocal<Integer> local;
    public static void main(String[] args) {

        Thread A = new Thread(new ThreadA());
        A.start();
        ThreadB B = new ThreadB();
        B.start();

    }

    static class ThreadA implements Runnable{
        @Override
        public void run() {
            local = new ThreadLocal();
            local.set(a+10);
            System.out.println(local.get()+Thread.currentThread().getName());
            local.remove();
            System.out.println(local.get()+Thread.currentThread().getName());
        }
    }

    static class ThreadB extends Thread{
        @Override
        public void run() {
             System.out.println(local.get()+Thread.currentThread().getName());

        }
    }
}

Before we knew, ThreadLocal to provide us with a thread-local variables that we tested method is to create two threads, use ThreadLocal to access value, see whether this will affect each other between two threads above this our simple piece of code to analyze, first of two variables:

private static int a = 10;
    private static ThreadLocal<Integer> local;

Notice how here to use a ThreadLocal, usage, and common variable is almost the same, we can put this time in accordance with a ThreadLocal variable to understand, we usually define a variable that is not so:

int a = 10;

So for the ThreadLocal it is the same, just as we create a ThreadLocal to create a new variable, like:

 private static ThreadLocal<Integer> local;

This time we define a ThreadLocal, pay attention to this time and not just defined initialize assignment, not as int a = 10 has been assigned as 10, and now the ThreadLocal just good definition of it, we continue to look at the following code:

static class ThreadA implements Runnable{
        @Override
        public void run() {
            local = new ThreadLocal();
            local.set(a+10);
            System.out.println(local.get()+Thread.currentThread().getName());
            local.remove();
            System.out.println(local.get()+Thread.currentThread().getName());
        }
    }

    static class ThreadB extends Thread{
        @Override
        public void run() {
               System.out.println(local.get()+Thread.currentThread().getName());

        }
    }

Here is the definition of two threads, note read, in the run method of a thread, we ThreadLocal is instantiated:

local = new ThreadLocal();

Here, we have created a complete ThreadLocal, which is this:

ThreadLocal local = new ThreadLocal();

We can say ThreadLocal seen as a variable, like ordinary variables, such as this before following:

int a = 10;

In this way, we give a 10 assignment, then for ThreadLocal, we set the value of the how to do it? The following operations:

local.set();

As we did the above code:

local.set(a+10);

So we gave ThreadLocal to the assignment, then how to get the value of it? As shown in the code above:

System.out.println(local.get()+Thread.currentThread().getName());

That is, through:

local.get()

At this point, we know the most basic use of ThreadLocal.

Basic use

That is:

ThreadLocal local = new ThreadLocal();
local.set(a+10);
local.get()

Here we have not felt it like a map, but also to access the value of key-value in the form of it?

Further in the above code is a code there follows:

local.remove();

Ye Hao understand this, delete, delete Shane? Let's leave a doubt, the next article will slowly say, to see the end, you will understand.

Then there is the code we've shown so:

Thread A = new Thread(new ThreadA());
A.start();
ThreadB B = new ThreadB();
B.start();

This is to open two threads.

At this point, the code we've shown a bit on the simple analysis, focusing looked ThreadLocal is a simple to use.

Then this code will output the result? Before looking at the output, we need to emphasize that, ThreadLocal can provide local variables within the thread, interference between each thread. We think that the code shown above. The first is the definition of ThreadLocal:

Next instantiated in the first thread and assignment:

Then we look at the second thread:

Big eyes to see, looks like it should still feel 20, after all, is a local with ah, but local has been previously assigned equal to 20, but here to get this value again in another thread, we look at the output:

See the results we know, though ThreadLocal is instantiated and assigned in the first thread, and the correct value of 20, but in another thread to an empty value, then, let us change slightly under the code:

Oh, seems to understand, for ThreadLocal, each thread is to have a separate existence, the equivalent of a copy simultaneously and thread, there is also a null because the call:

local.remove();

This is equivalent to the value deleted, nature is empty, think about the above results do not just explain the role of ThreadLocal do? Provides thread-local variables, each thread has its own copy, no effect between threads.

Some people may not understand, local where not all this?

Is not it the same? It stands to reason that a ah, in another thread should be the same as the value of ah, how is it empty? And in another thread just call this simple method ah get:

local.get()

Oh, I know, this is probably get problems between different threads to realize get is different, and that its bottom is how to achieve it?

3. The principle of ThreadLocal

Source interpret get method

Well, surely someone can not wait to get a look at this is how to achieve, and why the above results appear, then we take a look at the underlying source of this get:

This is the realization of the get method, we first glance may not fully understand every detail, but the general meaning is very clear, and then we come simple analysis of the problem we are to solve is why another thread after calling the get method to get the value is null, which is this:

We first look at these two codes:

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);

首先是获取当前线程,然后根据当前线程得到一个ThreadLocalMap,这个ThreadLocalMap是个啥,我们暂时还不知道,解下来就进行了如下判断:

if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }

也就是在判断根据当前线程得到的ThreadLocalMap是否为空,我们想想,我们就是直接调用get就来到了这里,好像并灭有什么地方去创建了这个ThreadLocalMap吧,那么这里判断的就是空了,所以就会去走如下的语句:

return setInitialValue();

虽然这里我们并没有这个Map,但是我们看如果有map的话是怎么执行呢?我们仔细看看这段代码:

ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }

这不就是在返回我们需要的值嘛?这个值是从这个ThreadLocalMap中拿到的,哦,到了这里似乎知道了,为啥在另一个线程中调用get会得到null,那是因为值被放到了一个叫做ThreadLocalMap的东西里面了,而它又是根据当前线程创建的,也就是说每个线程的ThreadLocalMap是不同的,在当前线程中并没有创建,所以也就没值。

为什么是null?

嗯嗯,这个想法貌似很对,不过又有个问题,为啥会是null呢?我们就要看这个语句的执行了:

return setInitialValue();

从这个方法的名字可以猜想,这应该是初始化操作的。我们看看这方法是如何实现的:

在这个方法之中,首先会执行如下语句:

T value = initialValue();

我们看看这个方法的实现:

原来就返回一个null啊,那么上面的value就是null了,然后我们再看下面的语句,是不是觉得很熟悉:

我们知道,这里map是没有的,所以会走else,也就是回去执行如下的方法:

createMap(t, value);

对了,这里的value是个null,而t就是当前线程啦,我们继续看看这个方法的实现:

哦,看到这里似乎就知道,在这个方法中就创建了一个ThreadLocalMap,我们之前看源码觉得数据是被放到了这个ThreadLocalMap中了,那么现在这里已经创建了一个ThreadLocalMap,那么数据是哪个呢?看方法实现,应该就是那个firstValue了,我们知道这个值就是之前传过来的value,也就是null,这相当于一个value值,那么这里的key呢?是不是就是这个this,那么这个this指的谁呢?

这里的this其实是ThreadLocal的实例,也就是之前的local:

所以到了现在,这个get方法的我们分析的结果就是创建了一个ThreadLocalMap,然后往里面放了值,是一个key-value的形式,key就是我们的ThreadLocal实例。

然后我们再看执行完createMap之后的操作,就是直接返回value了,也就是一个null,所以现在我们明白了为什么这里调用get是个null。

看到这里,这个get是明白怎么回事了,那么在第一个线程中的get也是这样的吗?

源码解读set方法

对于get的方法实现肯定是一样的,之所以这里得到值20,那是因为在当前线程执行了set方法:

local.set(a+10);

根据我们之前对get的分析,这里我们应该可以猜想到,set方法也创建了一个ThreadLocalMap并且把值放了进去,所以执行get能得到值,我们一起来看下set的实现:

是不是很熟悉,也是先拿到当前线程,然后根据当前线程得到ThreadLocalMap,这里同样之前没有,所以需要重新创建,也就是去执行:

createMap(t, value);

但是这里的value就不是null了,而是传过来的20,我们接着看这个方法的实现:

熟悉不,又到了这里,创建了一个新的ThreadLocalMap来存放数据,this同样也是ThreadLocal的实例,也就是local,这样一来,key就对应我们的ThreadLocal实例,value就是传过来的20了,另外我们大概知道,这么个键值对是放在ThreadLocalMap中的,然后我们通过当前线程可以得到这个ThreadLocalMap,再根据ThreadLocal这个实例就可以得到value的值,也就是20.

我们接下来看这个线程中的get的执行:

因为我们在set的时候就创建了ThreadLocalMap,所以这里就不会再去创建了,因为已经有map了,所以会直接执行:

ThreadLocalMap的源码解读

这里其实就牵涉到ThreadLocalMap的内部实现了,看到这里我们需要来借助一张图以便加深理解,就是下面的这张图:

经过我们上面的分析,我们知道ThreadLocal的设置值的方式是key-value的形式,也知道了这里的key其实就是ThreadLocal的实例,value就是要设置的值。

这里我们看下ThreadLocalMap,它其实是一个数据结构,就是用来存放我们的值的,而且它也是ThreadLocal的一个核心,我们通过上面这张图,首先要知道的一点就是:

ThreadLocalMap中存储的是Entry对象,Entry对象中存放的是key和value。

至于为什么是这样的,我们一步步的来分析ThreadLocalMap!

ThreadLocalMap中的Entry

在ThreadLocalMap中其实是维护了一张哈希表,这个表里面就是Entry对象,而每一个Entry对象简单来说就是存放了我们的key和value值。

那么这个是如何实现的呢?首先我们来想,Entry对象是存放在ThreadLocalMap中,那么对于TreadLocalMap而言就需要一个什么来存放这个Entry对象,我们可以想成一个容器,也就是说ThreadLocalMap需要有一个容器来存放Entry对象,我们来看ThreadLocalMap的源码实现:

在ThreadLocalMap中定义了一个Entry数组table,这个就是存放Entry的一个容器,在这里我们首先需要知道一个概念,那就是什么是哈希表

百度百科是这样解释的:

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

上面也提到过,ThreadLocalMap其实就是维护了一张哈希表,也即是一个数组,这个表里面存储的就是我们的Entry对象,其实就是它:

哈希表扩容

涉及到哈希表,必然会涉及到另外一个概念,那就是增长因子,那什么是增长因子呢?

简单来说,这是一个值,当表里面存储的对象达到了表的总容量的某个百分比的时候,这张表就该扩容了,那么这个百分比就是增长因子,我们看ThreadLocalMap中的增长因子:

从这些代码我们可以了解到,ThreadLocalMap中定义了一个threshold属性,这个属性上面有个介绍,也就是:

The next size value at which to resize.

翻译过来就是:要调整大小的下一个大小值。

什么意思呢?也就是说当哈希表中存储的对象的数量超过了这个值的时候,哈希表就需要扩容,那么这个值具体是多大呢?下面有个方法:

它也有个注释:

Set the resize threshold to maintain at worst a 2/3 load factor.

翻译过来就是:设置调整大小阈值以保持最坏的2/3负载系数。

意思就是设定这个增长因子为总容量的2/3,这个增长因子就是threshold。也就是当哈希表的容量达到了总容量的2/3的时候就需要对哈希表进行扩容了。

Entry对象是如何存储数据的

到这里我们就知道了,ThreadLocalMap维护了一个哈希表,表里面存储的就是Entry对象,当哈希表的当前容量达到了总容量的2/3的时候就需要对哈希表进行扩容了。

那么可能有人会问了,初始容量是多少啊?这个在源码中也有展现:

也即是16,那么对于数据而言,它又是怎样被放到哈希表中的呢?接下来我们就来看看ThreadLocalMap中设置值的方法:

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

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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();
        }

我们来一步步的分析这段源码,看看数据是如何被存储的,为了让大家更加的明白,我们还是从最开始的ThreadLocal设置值得时候开始一步步的进入到这段源代码,首先就是这段代码:

这是在第一个线程中,我们对ThreadLocal进行了实例化,并且在第一个线程总开始设置值,也就是调用set方法,我们进入到这个set方法看看:

我们之前就分析过了,这里没有map,会去创建,我们进入到createMap中看看:

这里创建了ThredLocalMap,调用了它的构造方法,我们进入看看:

这段代码就需要好好解读了,首先是它:

table = new Entry[INITIAL_CAPACITY];

这个table没有忘记是啥吧,就是之前定义的Entry数组,就是这个:

这里的INITIAL_CAPACITY就是初始化容量16,所以这里就构建了一个容量为16的Entry数组,这个数组就可以用来存放我们的数据,具体怎么存放,我们接着往下看:

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

这里是为了得到一个下表,因为哈希表是依靠一个索引去存取值得,所以会根据这个下标值去决定把数据存放到哪个位置,简单点就是把数据放到数组中的哪个位置,这个就是数组下标,那这个threadLocalHashCode是个啥呢?我们看看:

它是通过这个nextHashCode方法得到的,这个nextHashCode也有一系列的操作,反正最终目的就是为了得到一个索引值,或者是下标值,来决定这个数据存放到哪个位置。

那为什么这样写呢?

firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

这是拿得到的threadLocalHashCode对Entry数组的总容量减去一的值做取余操作,目的就是为了得到的下标值始终都在数组内,防止下标越界的。

再接着看剩下的代码:

table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);

拿到下标值之后就得到了一个位置就是table[i],然后就是把一个具体的Entry对象放进去了,剩下的就是设置当前表中有几条数据,也就是有几个Entry对象了,然后根据初始容量设置增长因子,我们重点来看看这段代码:

table[i] = new Entry(firstKey, firstValue);

table[i]也就是Entry数组中的一个确切的位置,是要放入一个Entry对象的,这里就new了一个新的Entry对象,并把key和value传入了进去,我们看看这个Entry的构造方法以及这个Entry类的实现。

Entry长啥样?

我们先来看看它的这个构造函数:

这其实也是Entry类的源码,其中有一个构造函数,传入key和value,在Entry中还定义了一个Object类型的value变量,把随构造函数传入进来的value值赋值给这个Object类型的value变量,这样就将value保存在了Entry中了。

我们再来看这个Entry的实现,它是继承了WeakReference<ThreadLocal<?>>,这个是啥?WeakReference<ThreadLocal<?>>是一个弱引用类型,简单说,Entry本质上就是一个弱引用,因为是继承WeakReference<ThreadLocal<?>>这个弱引用,所以它其实也是个弱引用,而Entry的实例说白了就是对ThreadLocal实例的一个弱引用,只不过Entry的设计上同时还保存了value值。

到这里,我们就知道了这个Entry是如何保存键值对的了,也知道Entry其实就是个弱引用。

对了,你要知道上述我们的分析是针对ThreadLocal第一次调用set方法的时候因为没有map需要创建map走得上述方法,如果是再次调用则会走map中的set方法,我们具体看源码:

由于我们在第一次调用set方法时已经创建了map,那么再次set的时候就会主席那个map的set方法,我们来看看map的set方法是如何实现的:

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

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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();
        }

这就是ThreadLocalMap中通过set方式设置值的源码实现,第一次调用是通过构造函数的形式设置数据,我们现在来看看这个set方式的数据设置。

Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

首先是拿到之前创建的Entry数组,这里是tab,然后也是计算出一个新的下标值来存放新数据,接下来就是这段代码:

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;
                }
            }

首先要知道这是一个for循环,根据一个下标值得到一个新的Entry对象,然后进入循环条件 也即是这个Entry对象不为null,然后执行循环体,循环体中有两个判断,还有一个根据当前Entry对象得到ThreadLocal的引用,也即是Key,不过这里定义为k。

现在我们要知道,我们是要往Entry数组中放入一个新的Entry对象,具体放到哪里由i这个下标值确定,具体的位置就是table[i],所以会出现的情况就有这个位置原本就有一个Entry对象或者为null,于是如果原本就有的话而且引用的是同一个ThreadLocal的话,那么就把值给覆盖掉:

if (k == key) {
                    e.value = value;
                    return;
                }

如果是这个位置是null的话,我们就放入新的值:

if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }

当然,也会出现的情况就是这个位置不为null,而且也不是同一个ThreadLocal的引用,那么就需要继续往后挪一个位置来放入新的数据:

e = tab[i = nextIndex(i, len)])

当然,这个新的位置上依然要进入判断,也是上面的情况,以此类推,直到找到一个位置要么为null,要么是同一个ThreadLocal的引用,只有这样才能放入新的数据。

我们接着来看下面的代码,执行完上面的判断之后会执行如下的代码:

 tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();

这个就是创建具体的Entry对象,因为Entry数组多了一个Entry对象,所以总条目需要加一,而这个if判断则是为了看看当前存储的对象个数是否达到了增长因子,也就是判断下是否需要扩容,如果需要扩容了该怎么办呢?这个时候要依靠的就是这个rehash函数了。

rehash函数是如何实现重新扩充并重新计算位置的

如果达到了增长因子,那就需要重新扩充,而且还需要将所有的对象重新计算位置,我们来看rehash函数的实现:

我们看到在if判断中判断的指标是增长因子的3/4,这是怎么回事,之前不是说增长因子是2/3嘛?超过这个值才需要扩容,这怎么变成了增长因子的3/4才开始扩容呢?我们之前说过,ThreadLocalMap中存储的是Entry对象,Entry本质上是个ThreadLocal的弱引用,所以它随时都有可能被回收掉,这样就会出现key值为null的Entry对象,这些都是用不到的,需要删除掉来腾出空间,这样一来,实际上存储的对象个数就减少了,所以后面的判断就是增长因子的3/4,而不是增长因子2/3了。

而expungeStaleEntries();就是做这样的清理工作的,把占坑的Entry统统删除掉。

如何获取Entry对象中的数据

那该如何获取到Entry对象中的数据呢?也即是我们使用ThreadLocal的实例去调用get方法取值:

因为已经有map了,所以我们直接就调用map的getEntry方法,我们看看这个方法的实现:

这段代码还是比较简单的,首先根据哈希码值算出下标i,然后就确定了这个Entry的位置,如果这个位置不为空而且对用的ThreadLocal的弱引用也是我们需要的这个,那么就返回这个Entry对象中保存的value值。

当然,如果对应的位置为空的话,我们就需要getEntryAfterMiss函数来进行进一步的判断了。

到了这里相信大家对ThreadLocalMap就有了一定的认识了,接下来我们继续来聊聊ThreadLocal的内存泄露问题。

4、ThreadLocal的内存泄露

什么是内存泄漏和内存溢出

我们在讲ThreadLocal的内存泄漏之前,首先要搞清楚什么是内存泄漏,那要说起内存泄漏,肯定还有个概念需要说,那就是内存溢出,这两者是个啥呢?

首先什么是内存泄漏

说的简单点那就是因为操作不当或者一些错误导致没有能释放掉已经不再使用的内存,这就是内存泄漏,也就是说,有些内存已经不会再使用了,但是却没有给它释放掉,这就一直占用着内存空间,从而导致了内存泄漏。

那什么是内存溢出呢?

这个简单点说就是内存不够用了,我运行一个程序比如说需要50M的内存,但是现在内存就剩下20M了,那程序运行就会发生内存溢出,也就是告诉你内存不够用,这时候程序就无法运行了。

好,了解了基本概念之后,我们再来看看T和read Local的内存泄漏,那为什么T和read Local会产生内存泄漏呢?我们再来看看这张图:

经过我们上述的讨论,我们大致知道了ThreadLocal其实本质上是在每个线程中单独维护了一个ThreadLocalMap数据结构,这个ThreadLocalMap是每个线程独有的,只有根据当前线程才能找到当前线程的这个ThreadLocalMap,这就实现了线程之前的隔离。

我们看上面那张图,每个线程根据找到自己维护的ThreadLocalMap,然后可以操作这个数据结构,往里面存取数据,而ThreadLocalMap中维护的就是一个Entry数组,每个Entry对象就是我们存放的数据,它是个key-value的形式,key就是ThreadLocal实例的弱引用,value就是我们要存放的数据,也就是一个ThreadLocal的实例会对用一个数据,形成一个键值对。

如果有两个线程,持有同一个ThreaLocal的实例,这样的情况也就是Entry对象持有的ThreadLocal的弱引用是一样的,但是两个线程的ThreadLocalMap是不同的,记住一点,那就是ThreadLocalMap是每个线程单独维护的。

为什么会出现内存泄漏

那我们现在来看,为什么ThreadLocal会出现内存泄漏,我们之前也说过了,Entry对象持有的是键就是ThreadLocal实例的弱引用,弱引用有个什么特点呢?那就是在垃圾回收的时候会被回收掉,可以根据上图想一下,图中虚线就代表弱引用,如果这个ThreadLocal实例被回收掉,这个弱引用的链接也就断开了,就像这样:

那么这样在Entry对象中的key就变成了null,所以这个Entry对象就没有被引用,因为key变成看null,就取不到这个value值了,再加上如果这个当前线程迟迟没有结束,ThreadLocalMap的生命周期就跟线程一样,这样就会存在一个强引用链,所以这个时候,key为null的这个Entry就造成了内存泄漏。

因为它没有用了,但是还没有被释放。

如何解决内存泄漏

明白了如何产生的内存泄漏,也就知道了怎么解决,经过上面的分析,我们大致知道了在ThreadLocalMap中存在key为null的Entry对象,从而导致内存泄漏,那么只要把这些Entry都给删除掉,也就解决了内存泄漏。

我们每次使用ThreadLocal就会随线程产生一个ThreadLocalMap,里面维护Entry对象,我们对Entry进行存取值,那么如果我们每次使用完ThreadLocal之后就把对应的Entry给删除掉,这样不就解决了内粗泄漏嘛,那怎么做呢?

在ThreadLocal中提供了一个remove方法:

这个就是根据key删除掉对应的Entry,如此一来,我们就解决了内存泄漏问题,因为可能出现内存泄漏的Entry,在我们使用完之后就立马删除了。

所以对于ThreadLocal而言,就应该像使用锁一样,加锁之后要记得解锁,也就是调用它的remove方法,用完就清理。

5、总结

至此,我们已经对ThreadLocal做了一个较为全面和深入的分析,大家应该也对它有了更深的印象,下面针对本文来做一个简单的总结:

1、ThreadLocal是用来提供线程局部变量的,在线程内可以随时随地的存取数据,而且线程之间是互不干扰的。

2、ThreadLocal实际上是在每个线程内部维护了一个ThreadLocalMap,这个ThreadLocalMap是每个线程独有的,里面存储的是Entry对象,Entry对象实际上是个ThreadLocal的实例的弱引用,同时还保存了value值,也就是说Entry存储的是键值对的形式的值,key就是ThreadLocal实例本身,value则是要存储的数据。

3、TreadLocal的核心是底层维护的ThreadLocalMap,它的底层是一个自定义的哈希表,增长因子是2/3,增长因子也可以叫做是一个阈值,底层定义为threshold,当哈希表容量大于或等于阈值的3/4的时候就开始扩容底层的哈希表数组table。

4、ThreaLocalMap中存储的核心元素是Entry,Entry是一个弱引用,所以在垃圾回收的时候,ThreadLocal如果没有外部的强引用,它会被回收掉,这样就会产生key为null的Entry了,这样也就产生了内存泄漏。

5、在ThreadLocal的get(),set()和remove()的时候都会清除ThreadLocalMap中key为null的Entry,如果我们不手动清除,就会造成内存泄漏,最佳做法是使用ThreadLocal就像使用锁一样,加锁之后要解锁,也就是用完就使用remove进行清理。

6、关于原创

本文原创作者:ithuangqing
转载请注明出处,微信公众号开白请联系我微信H653836923

▼ 庆哥有一个梦想,写一些能让小白看得懂学得会的技术教程,帮助初学者更快的入门与进阶,于是乎,在编码之外开启了逐梦之旅!关注公众号,后台回复“庆哥”,2019最新java自学资源立马送上!

在这里插入图片描述

长按二维码识别关注!

Guess you like

Origin www.cnblogs.com/ithuangqing/p/12114581.html