进阶良药之ThreadLocal

同学,你是否被面试官考察过这些问题:

  1. 什么是ThreadLocal,能说说你的理解吗?
  2. ThreadLocal类有哪些方法,它们都是做什么的?
  3. ThreadLocal变量和synchronized关键字的区别是什么?
  4. 你能想到哪些使用ThreadLocal的典型场景?
  5. ThreadLocal为什么会导致内存泄露?

上面的五个问题是Java面试中考察ThreadLocal变量时的经典问题。要完美的回答这些问题,咱还得从头说起!下面是JDK文档对ThreadLocal的解释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.
翻译:ThreadLocal类提供线程本地变量。线程本地变量和普通的变量不同,每一个访问(通过get或者set方法)线程本地变量的线程都将拥有一个属于它们自己的独立初始化的副本。

JDK文档说的很清楚,ThreadLocal类用来声明一个线程本地变量。每有一个访问该变量的线程,就会对应创建一个该变量的副本。

面试中,如果你仅仅是这么解释,可能面试官会觉得还不够。是时候聊一聊源码了。下面是ThreadLocal类的部分源码(以set方法为例):

public class ThreadLocal<T> {

    public void set(T value) {
        Thread t = Thread.currentThread();
        java.lang.ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  
    java.lang.ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new java.lang.ThreadLocal.ThreadLocalMap(this, firstValue);
    }

}

ThreadLocal的set()方法在执行时,会首先使用Thread.currentThread()获取执行当前方法的线程t,然后获取t的成员变量threadLocals。如果threadLocals不存在,则为其创建一个ThreadLocalMap对象并赋值给它;如果threadLocals存在,则直接把要保存的对象放入其中。

从本质上来讲,ThreadLocal更像是一个工具类。我们对ThreadLocal对象的操作最终被转化为对当前线程的threadLocals成员变量的操作。即,对一个ThreadLocalMap对象进行存取操作。

费了“九牛二虎之力”,总算回答了第一个问题。憋急!我们来看第二个问题。

根据JDK文档,ThreadLocal类有四个常用的方法:

T get()

该方法用于返回线程本地变量在当前线程中的值。如果当前线程中没有该变量的值,则先调用initialValue()方法执行初始化,然后再返回对应的值。

T initialValue()

如果当前线程在执行get()方法之前没有执行过set()方法,那么initialValue()方法将被调用,反之initialValue()方法将不会被调用,如果initialValue()方法被调用的时候,将会把null当做value值存在ThreadLocalMap中,之后把null作为get()方法的返回值在返回给你,这个可以直接看源码

void remove()

删除某个线程本地变量在当前线程中的值。如果删除之后,再一次读取该线程本地变量的值,会重新执行initialValue()方法对其进行初始化。

void set(T value)

设置线程本地变量在当前线程中保存的值。

读了上面四个方法的定义,是不是再一次印证了我之前说的?我们可以把ThreadLocal理解为一个工具类,它帮助我们去操作当前线程中的一个Map结构的成员变量。这个Map结构的成员变量是线程私有的。

讲解到这里,第三个问题的答案就呼之欲出了!

ThreadLocal变量和synchronized关键字都是用来解决线程安全问题的,但是两者解决问题的思路完全不同。ThreadLocal使得每个线程都有自己的局部变量资源,因此多个线程之间互不干扰,从而实现线程安全。synchronized关键字保证每个时刻只能有一个线程访问共享变量,从而保证了线程安全。

对于ThreadLocal这么“有料”的知识点,面试官自然要考察下求职者的实战能力了。反正,我是不会错过的。同学,说说哪些场景下会用到ThreadLocal变量?

1. Request粒度的信息保存

Java Web是一个单例多线程的模型。

即通常情况下,Web程序中的每一个Bean都是单例,而用户的每次请求都会对应一个独立的线程,当多个用户同时访问Web程序时,表现出来的就是单例多线程。

在这种模型中,如果需要在一次请求周期中保存某些用户信息,那这个信息绝对不能放到类的成员变量中去。因为类的成员变量是隶属于同一个Bean的,而Bean是被多个线程所共享的,会导致多个线程更改同一个成员变量的情况,程序表现为一会正常一会不正常。这也是实践中经常遇到的bug。

对于这种场景,就要使用ThreadLocal类了。通常的做法是在请求进入Bean之前把相关的信息保存到ThreadLocal变量中,待后续需要的时候从ThreadLocal中获取即可。

2. Spring框架的事务处理

大家都知道,Spring允许以声明的方式进行事务管理。通过声明的方式,程序员可以仅仅专注于业务代码,事务管理由Spring框架代为进行。那么Spring是如何做到这一点的呢?

以JDBC为例,正常的事务代码如下。该代码可以分成三个部分:事务准备阶段(第1~3行)、业务处理阶段(第4~6行)、事务提交阶段(第7行) 。

jdbc = new DataBaseConnection();//第1行
Connection con = jdbc.getConnection();//第2行
con.setAutoCommit(false);//第3行
con.executeUpdate(...);//第4行
con.executeUpdate(...);//第5行
con.executeUpdate(...);//第6行
con.commit();//第7行

Spring框架包揽了事务准备阶段和事务提交阶段的代码,使得程序员专注于设计业务处理阶段的代码。Spring框架可以使用AOP(Aspect Oriented Programming)来动态的织入准备阶段和事务提交阶段的代码。但如何才能让三个阶段使用同一个数据源连接呢?这是很重要的。

在这个场景中,我们实际上是希望让软件结构中纵向的三个阶段使用同样的一个参数con,而这三个阶段之间又无法进行显式的参数传递。解决方案是—ThreadLocal。Spring框架使用ThreadLocal记录每个线程在进行事务操作时所使用的数据库连接,在事务准备阶段,将数据库连接放入ThreadLocal中,在业务处理阶段和事务提交阶段直接从ThreadLocal中获取对应的连接即可。这个是ThreadLocal的经典应用。

以上讲解了两个ThreadLocal在实践中的应用。如果在面试中,你不但能够讲清楚ThreadLocal的底层原理,还能够结合具体的实际娓娓道来,比如Spring框架中是如何使用ThreadLocal的。相信面试官会对你大加欣赏!

然后我们回到第5个问题,当你使用ThreadLocal变量久了之后,可能会遇到内存泄露的问题。不要着急,这说明你距离一个老司机只有一步之遥了。

假设我们有这样一段代码:

public class Test {
    public static final ThreadLocal<String> tl = new ThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return "hello";
        }
    };

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(tl.get());
            }
        });
        t.start();
    }
}

这段代码执行后,内存中是这样的:

在这里插入图片描述

如上文所述,threadLocals变量实际上是ThreadLocal.ThreadLocalMap类型的对象。它本质上是一个Map结构,只不过这个Map结构中的Key是一个ThreadLocal类型的弱引用。从图中可以看到,tl的存储空间有两个引用,一个是强引用,一个是来自于ThreadLocalMap的弱引用。当线程执行完毕,栈空间被销毁以后,强引用不复存在,而弱引用并不会妨碍GC对tl存储空间的回收,这从一定程度上避免了内存泄露。

但我们也不能忽略一个事实,当tl的存储空间被垃圾回收以后。ThreadLocalMap中原本指向tl的Entry,其key的值将变成NULL。即,内存中将存在一些key为NULL的Entry。这部分Entry也是需要被垃圾回收的。ThreadLocalMap在设计时也考虑到了这些问题,并添加了防护措施:在调用的get()、set()、remove()的时候都会顺便清除线程ThreadLocalMap里面所有key为NULL的Entry。

但是作为JAVA程序员的我们需要清楚,即便Java在设计TheadLocal时做出了这么多预防内存泄露的努力,也还是我们显式调用remove()方法来的更加踏实一些。

所以你永远要记住使用ThreadLocal的最佳实践:

每次使用完ThreadLocal之后,都要调用它的remove()方法,清除数据。

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值