赞
踩
为了提升程序运行的效率,多线程的使用是必不可少的。
那么提到多线程的使用,有一个知识点是我们必然无法绕过的,那便是线程之间的通信方式。
线程之间的通信方式主要有以下这些:
那么在本文中我们来重点讲解一下ThreadLocal的原理和使用场景。
顾名思义,Thread->线程,Local->本地变量。所以ThreadLocal是线程本地变量的意思,当我们创建了一个ThreadLocal变量的时候,访问这个变量的每个线程都会有这个变量的一个本地拷贝,当多个线程来操作这个变量的时候,实际操作的是本地内存里面的变量,通过这种方式实现线程隔离,避免多线程导致的数据安全问题。
我们可以用一张图来看一下TheadLocal的位置:
通过以下语句就可以创建一个ThreadLocal变量,在线程对共享变量进行修改时,实际上修改的就是这个变量副本,通过这种空间换时间的方式,来保证线程的安全。
arduino复制代码public static ThreadLocal<String> localVar = new ThreadLocal<>();
我在工作中的以下场景中用到了ThreadLocal
当用户登录后访问接口,会从前端携带必要的登录信息,存放在Cookie中,请求经过统一的网关后,我们会解析出用户的ID。当请求走入业务代码时,用户可能会执行一些操作。而在这些操作中会包含一些公用的信息。例如,如果是B端的用户,我们需要获取到用户的公司,用户的身份,角色,权限等等。而如果是C端用户,则需要解析出用户是否VIP,是否用某些内容的查看权限等等。
这些公用的信息如果我们放在具体的服务层中去做则会产生大量的冗余代码,降低代码的可读性,而且如果后续如果一些公用的规则有所更改,例如权限校验的规则有所改变,那么我们需要大范围的更改代码,这个过程也十分容易产生遗漏。
因此通过TheadLocal封装用户公共的上下文信息就是一个非常好的办法,这样我们可以将身份鉴定、权限等一系列公用内容统一处理,服务层直接应用即可。
第二个业务场景是这样子,我们用一个实体A,要经过一系列校验规则后确定是否能够转换为实体B。这是一个典型的参数校验的场景,在这个业务场景中我们使用了责任链的设计模式。
在初期使用责任链时,我们的整个请求是串行的。这种设计模式的应用让我们对于校验规则的拆分十分细致,但是在不断的迭代中也出现了一些问题。其中最为显著的问题便是在维护责任链的过程中,每次的改动可能都存在于一个类里面(一个类既是一个单独的校验规则),但是当规则越来越多的时候,不可避免的会出现业务重叠,即某个第三方服务可能会在不同校验规则里面多次调用。这样子导致了第三方服务的压力,同时也使得我们的请求时间变长。
我们的解决方案即是在参数校验的责任链中增加一个统一上下文信息。
假设在链式场景中有三个校验规则,而这三个校验规则的实体类中又有这一些公共信息,我们使用ThreadLocal来存放这些公共信息的值。
因为这个参数校验流程是非常底层的服务,QPS很高,在保障服务的高可用性的同时也需要尽量的降低请求时间。所以使用多线程便是一个非常必要的途径。通过ThreadLocal存放一些公用信息作为上下文,首先可以保证多线程请求下的数据安全,并且我们可以有效降低对其他服务的请求次数,同时也缩短了响应的时长。
那么讲了这么多,我们一起来看一下ThreadLocal在Java中是如何实现的。
我们首先来看源码中ThreadLocal最重要的两个方法:get 和 set方法
- ini复制代码 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();
- }
- scss复制代码 public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- map.set(this, value);
- } else {
- createMap(t, value);
- }
- }
可以看到,两个方法中都指向了一个非常重要的变量 —— getMap,我们看一下这个getMap获取到的是什么:
可以看到,是一个叫做ThreadLocalMap的变量,那么既然叫做Map,则必定是<key,value>的结构,继续探究,这个map的键值分别是什么:
我们可以看到,其中的key就是一个ThreadLocal,value是我们在代码中放入自定义的值。
看到这里,我们来归纳一下,ThreadLocal是如何实现线程隔离的?
在刚才的源码中还有一个点我们需要注意的就是ThreadLocalMap中的key继承了一个弱引用(WeakReference),Java中的四种引用我们一起回忆一下:
我们运行一个Demo看一下:
- typescript复制代码public class WeakReferenceTest {
- public static void main(String[] args) {
- Object object = new Object();
- WeakReference<Object> testWeakReference = new WeakReference<>(object);
- System.out.println("GC回收之前,弱引用:"+testWeakReference.get());
- //触发系统垃圾回收
- System.gc();
- System.out.println("GC回收之后,弱引用:"+testWeakReference.get());
- //手动设置为object对象为null
- object=null;
- System.gc();
- System.out.println("对象object设置为null,GC回收之后,弱引用:"+testWeakReference.get());
- }
- }
- ——————————————————————————————————————————————————————————————————————————————
- 运行结果:
- GC回收之前,弱引用:java.lang.Object@ec857b21
- GC回收之后,弱引用:java.lang.Object@ec857b21
- 对象object设置为null,GC回收之后,弱引用:null
那么现在问题就来了,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这:ThreadLocalMap的key没了,value还在,这就会造成了内存泄露。
如何解决内存泄露问题呢?这就需要我们在使用完ThreadLocal后,即时手动remove()释放内存的空间了。
示例代码:
- csharp复制代码public Result checkField(List<String> list, UserContext userContext) {
- myThreadLocal.set(list);
- try{
- // 业务代码
- }catch (Exception e){
-
- }finally {
- myThreadLocal.remove();
- }
-
- return Result;
- }
那如果不用弱引用,使用强引用呢?
key设计成弱引用同样是为了防止内存泄漏。
假如key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLoca 的强引用就没有了,但是此时key还强引用指向ThreadLoca,就会导致ThreadLocal不 能被回收,这时候就发生了内存泄漏的问题。
好了,本篇ThreadLocal的相关知识就分享到这里了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。