听说BAT这样的大公司,面试经常拿 ThreadLocal 考验求职者?(少侠留步)

今天,我们就来完完整整的学习下Threadlocal,争取以后再也不学了,因为看完今天这篇文章,你就对Threadlocal忘不了了!

ThreadLocal 在线程中可以给我们提供一个线程内的本地局部变量,这样就可以减少在一个线程中因为多函数之间的操作导致共享变量传值的复杂性。

说白了,我们使用ThreadLocal可以做到在一个线程内随时随地的取用,而且与其他的线程互不干扰。

在一些特殊的情景中,应用ThreadLocal会带来极大的便利,不过很多人却搞不懂Threadlocal到底是个啥?在我们的面试中也经常会被问到Threadlocal,所以基于我们的实际应用以及应对面试,我们都有必要好好的学习下Threadlocal。

1、什么是Threadlocal?

我们既然要学习Threadlocal,那么我们先要知道它是个啥?我们从名字来看,Threadlocal意思就是线程本地的意思,官方是怎么定义它的,我们看看ThreadLocal的源码(基于jdk1.8)中对这个类的介绍:

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).

这是在jdk1.8中对ThreadLocal这个类给的注释,我们简单翻译一下就是:

此类提供线程局部变量。这些变量与正常变量不同,因为每个访问一个线程(通过其{@code get}或{@code set}方法)的线程都有其自己的,独立初始化的变量副本。 {@code ThreadLocal}实例通常是希望将状态与线程相关联的类中的私有静态字段(例如用户ID或交易ID)。

什么意思呢?我们大致能够看明白,说是TreadLocal可以给我们提供一个线程内的局部变量,而且这个变量与一般的变量还不同,它是每个线程独有的,与其他线程互不干扰的。

现在我们简单的对ThreadLocal有了认识,下面我们就直接上代码,看看它的一个实际应用例子。

2、如何使用ThreadLocal?

2.1 方法介绍

ThreadLocal主要包含三个常用方法:

扫描二维码关注公众号,回复: 9546246 查看本文章
  • get():获取本线程中ThreadLocal的值
  • set():对本线程中ThreadLocal赋值
  • remove():删除本线程中的ThreadLocal对象

来看一段代码:

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

        }
    }
}

我们之前就知道,ThreadLocal是为我们提供一个线程局部变量的,那我们测试的方法就是创建两个线程,使用ThreadLocal去存取值,看看两个线程之间会不会互相影响。

注意看上面的这段代码,首先定义了两个变量,一个int,另一个ThreadLocal。

这个时候我们就定义了一个ThreadLocal,注意这个时候只是定义而没有进行初始化赋值,并不像int a = 10那样已经赋值为10了,现在的ThreadLocal还只是定义好而已,我们继续看下面的代码:

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

        }
    }

这里是定义了两个线程,注意看了,在第一个线程中的run方法内,我们对ThreadLocal进行了实例化,到这里,我们就完整的创建了一个ThreadLocal,也就是下面这样:

ThreadLocal local = new ThreadLocal();

然后,我们对ThreadLocal进行赋值,hreadLocal复制就像是Map一样,有自己的get()、set()方法,就像我们上面代码那样:

local.set(a+10);

取值当然就是get(),如同上面代码所示:

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

还有删除实例对象操作:

local.remove();

至此,我们所展示的代码就简单的分析了一下,重点看了ThreadLocal是个简单的使用。

2.2 结果分析

然后我们所展示的代码还有这么一段 -- main()方法:

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

这个就是开启两个线程。

那么这段代码会输出什么结果呢?在看输出之前,我们需要强调一点,ThreadLocal可以提供线程内的局部变量,各个线程之间互不干扰。那我们在思考上面所展示的代码。首先是定义ThreadLocal:

接下来在第一个线程中实例化并且赋值:

然后我们看在第二个线程中:

大眼一看,貌似觉得应该还是20,毕竟是同一个local啊,而且local在之前已经赋值了等于20,这里只不过在另外一个线程中再次去取这个值,我们来看看输出结果:

看到结果我们知道了,虽然在第一个线程中ThreadLocal被实例化且赋值了,而且正确取值20,但是在另一个线程中去取值的话为空,我们再来稍微改变下代码:

哦,似乎明白了,对于ThreadLocal而言,每个线程都是有一个单独存在的,相当于一个副本,线程之间互不影响

这里面还有一个null,是因为调用了 remove()方法:

local.remove();

这相当于把值删除了,自然为空,想一想,上述的结果不就说明了ThreadLocal的作用吗?提供线程局部变量,每个线程都有自己的一份,线程之间没有影响。

可能有的人不明白了,这里的local不都是这个吗?

难道不是同一个?按理说是一个啊,在另外一个线程中应该取值是一样的啊,怎么会是空呢?而且在另外一个线程中也只是调用了这个简单的get方法啊:

local.get()

哦,我知道了,这个可能就是get的问题,在不同的线程之间get的实现是不同的,那它的底层是怎么实现的呢?

这个问题不做过深研究,有兴趣的小伙伴可以自己查看源码,或者查看下面的微博:https://blog.csdn.net/sinat_33921105/article/details/103295070

我只简单介绍一句:和get()方法中的ThreadLocalMap有关,重点研究此类!!

3. 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是每个线程单独维护的。

3.1 为什么会出现内存泄漏

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

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

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

3.2 如何解决内存泄漏

明白了如何产生的内存泄漏,也就知道了怎么解决。

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

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

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

local.remove();

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

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

4. 总结

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进行清理

更多精彩,请关注我的"今日头条号":Java云笔记
随时随地,让你拥有最新,最便捷的掌上云服务

发布了151 篇原创文章 · 获赞 245 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_44259720/article/details/104535730
今日推荐