首页 > 其他分享 >ThreadLocal 简单介绍

ThreadLocal 简单介绍

时间:2023-04-15 17:34:44浏览次数:46  
标签:get 介绍 ThreadLocal 线程 key 简单 Entry null

目录

一、什么是ThreadLocal?

​ 从名字我们就可以看到 ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

二、ThreadLocal如何使用?

ThreadLocal的变量通常用private static修饰:

package com.linhuaming.test;

import java.util.HashMap;
import java.util.Map;

/**
 * 本地线程 测试
 */
public class ThreadLocalTest {

    private static ThreadLocal<Map<String,Object>> LOCAL = new ThreadLocal<>();

    //打印出本线程内的localVar值
    public static void printLoca(){
        Map<String, Object> map = LOCAL.get();
        System.out.println(map);

        //清除本地内存中的本地变量
        ThreadLocalTest.LOCAL.remove();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            Map<String,Object> map = new HashMap<>();
            map.put("username","lhm");
            map.put("password","123456");
            map.put("age",28);
            ThreadLocalTest.LOCAL.set(map);
            printLoca();
        }, "t1");
        t1.start();

        Thread.sleep(2000);

        Thread t2 = new Thread(() -> {
            Map<String,Object> map = new HashMap<>();
            map.put("username","wja");
            map.put("password","123456");
            map.put("age",29);
            ThreadLocalTest.LOCAL.set(map);
            printLoca();
        }, "t2");
        t2.start();
    }

}

注意:

  • ThreadLocal的泛型是Object的,可以定义成map、set、list等等
  • 一个类中可以定义多个ThreadLocal属性

运行结果:

三、ThreadLocal的实现原理是什么?

1、set()方法

实现过程:

  • 首先获取当前线程对象,并获取当前线程的ThreadLocalMap。
  • 如果这个map为null,就调用createMap()方法初始化map,初始化需要传入泛型和要存储的值。
  • 如果map不为null,就调用set的重载方法,它会将当前线程对象作为键,存储值。

如下图所示:

2、ThreadLocalMap

createMap()方法

其实本地线程对象的threadLocals属性,就是用来存储ThreadLocalMap的。

ThreadLocalMap是ThreadLocal的静态内部类。它用Entry保存数据,而且继承了弱引用,

Entry内部使用ThreadLocal类型的变量作为键,保存传入的值。

3、get()方法

实现过程:

  • 获取当前线程对象。
  • 如果map不为null,就通过ThreadLocal对象,取出对应的Entry。
  • 如果entry不为空,就获取Entry中的Value,返回。
  • 如果前一步中map为空,就调用setInitialValue()方法。

如下图所示:

setInitialValue()方法

这个方法是给ThreadLocal设置初始值,如下图所示:

4、remove()方法

将ThreadLocal的值,从当前线程的ThreadLocalMap中删除,如下图所示:

5、总结

​ 如果读懂了上述 get() 和 set() 方法,会发现 Map 存储在线程之上,在 ThreadLocal 中通过Thread.currentThread()来得到此线程,然后通过这个线程来获取这个线程对应的 Map ,而Map上的数据存储方式采用的是 <key,value> ,其中 key 的值是 ThreadLocal 自己,而value的值是我们想要获取的值,如下图所示:

img

四、ThreadLocal 数据共享

既然 ThreadLocal 设计的初衷是解决线程间信息隔离的,那 ThreadLocal 能不能实现线程间信息共享呢?
答案是肯定的,只需要使用 ThreadLocal 的子类 InheritableThreadLocal 就可以轻松实现,来看具体实现代码:

public static void main(String[] args) {
    ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
    inheritableThreadLocal.set("测试");
    new Thread(() -> System.out.println(inheritableThreadLocal.get())).start();
}

五、ThreadLocal在Java中的应用场景有哪些?

1、Spring实现事务隔离级别

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
 
private static final ThreadLocal<Map<Object, Object>> resources =
    new NamedThreadLocal<>("Transactional resources");
 
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
    new NamedThreadLocal<>("Transaction synchronizations");
 
private static final ThreadLocal<String> currentTransactionName =
    new NamedThreadLocal<>("Current transaction name");

2、解决日期的线程安全

项目中有部分用户的时间出错,发现是多个线程共享一个SimpleDataFormat的问题。

使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add()。

如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

但是每个线程内部都new一个SimpleDataFormat对象也不太好,所以使用ThreadLocal包装SimpleDataFormat,解决了线程安全的问题。

3、多个方法调用

一个线程经常需要横跨多个方法调用,那么它的参数就必须层层传递,给每个方法都加上相同的参数不太优雅。

而且,如果中间遇到第三方类库,参数就无法传递了。可以使用ThreadLocal,开始时把参数存进去,需要时直接get取出即可。

4、JDBC的数据库连接

从数据库连接池里获取的连接 Connection,在 JDBC 规范里并没有要求这个 Connection 必须是线程安全的。

数据库连接池通过线程封闭技术,保证一个 Connection 一旦被一个线程获取之后,在这个线程关闭 Connection 之前的这段时间里,不会再分配给其他线程,从而保证了 Connection 不会有并发问题。

六、常见问题

1、Entry的key为什么设计成弱引用?

前面说过,Entry的key,传入的是ThreadLocal对象,使用了WeakReference对象,即被设计成了弱引用。

那么,为什么要这样设计呢?

假如key对ThreadLocal对象的弱引用,改为强引用。

img

我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。

即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。

此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。

就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。

那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。

为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用

弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。

如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。

如下图所示:

img

接下来,最关键的地方来了。

由于当前的ThreadLocal变量已经被指向null了,但如果直接调用它的getsetremove方法,很显然会出现空指针异常。因为它的生命已经结束了,再调用它的方法也没啥意义。

此时,如果系统中还定义了另外一个ThreadLocal变量b,调用了它的getsetremove,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。

如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。

这样就能最大程度的解决内存泄露问题。

需要特别注意的地方是:

  1. key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用?
  2. 如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发getsetremove方法,也会造成内存泄露。

下面看看弱引用的例子:

public static void main(String[] args) {
    WeakReference<Object> weakReference0 = new WeakReference<>(new Object());
    System.out.println(weakReference0.get());
    System.gc();
    System.out.println(weakReference0.get());
}

打印结果:

java.lang.Object@1ef7fe8e
null

传入WeakReference构造方法的是直接new处理的对象,没有其他引用,在调用gc方法后,弱引用对象会被自动回收。

但如果出现下面这种情况:

public static void main(String[] args) {
    Object object = new Object();
    WeakReference<Object> weakReference1 = new WeakReference<>(object);
    System.out.println(weakReference1.get());
    System.gc();
    System.out.println(weakReference1.get());
}

执行结果:

java.lang.Object@1ef7fe8e
java.lang.Object@1ef7fe8e

先定义了一个强引用object对象,在WeakReference构造方法中将object对象的引用作为参数传入。这时,调用gc后,弱引用对象不会被自动回收。

我们的Entry对象中的key不就是第二种情况吗?在Entry构造方法中传入的是ThreadLocal对象的引用。

如果将object强引用设置为null:

public static void main(String[] args) {
    Object object = new Object();
    WeakReference<Object> weakReference1 = new WeakReference<>(object);
    System.out.println(weakReference1.get());
    System.gc();
    System.out.println(weakReference1.get());
    object=null;
    System.gc();
    System.out.println(weakReference1.get());
}

执行结果:

java.lang.Object@6f496d9f
java.lang.Object@6f496d9f
null

第二次gc之后,弱引用能够被正常回收。

由此可见,如果强引用和弱引用同时关联一个对象,那么这个对象是不会被GC回收。也就是说这种情况下Entry的key,一直都不会为null,除非强引用主动断开关联。

此外,你可能还会问这样一个问题:Entry的value为什么不设计成弱引用?

答:Entry的value假如只是被Entry引用,有可能没被业务系统中的其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致业务系统出现异常。

而相比之下,Entry的key,管理的地方就非常明确了。

这就是Entry的key被设计成弱引用,而value没被设计成弱引用的原因。

2、ThreadLocal真的会导致内存泄露?

通过上面的Entry对象中的key设置成弱引用,并且使用getsetremove方法清理key为null的value值,就能彻底解决内存泄露问题?答案是否定的。

如下图所示:

img

假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的getsetremove方法。

那么,Entry的value值一直都没被清空。

所以会存在这样一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。

其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露

3、如何解决内存泄露问题?

前面说过的ThreadLocal还是会导致内存泄露的问题,我们有没有解决办法呢?

答:有办法,调用ThreadLocal对象的remove方法。

不是在一开始就调用remove方法,而是在使用完ThreadLocal对象之后。列如:

先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。

public class CurrentUser {
    
    private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();
    
    public static void set(UserInfo userInfo) {
        THREA_LOCAL.set(userInfo);
    }
    
    public static UserInfo get() {
       THREA_LOCAL.get();
    }

    public static void remove() {
       THREA_LOCAL.remove();
    }
}

然后在业务代码中调用相关方法:

public void doSamething(UserDto userDto) {
   UserInfo userInfo = convert(userDto);

   try{
     CurrentUser.set(userInfo);
     ...

     //业务代码
     UserInfo userInfo = CurrentUser.get();
     ...

   } finally {
      CurrentUser.remove();
   }
}

需要我们特别注意的地方是:一定要在finally代码块中,调用remove方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。

remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。

参考:

https://blog.csdn.net/m0_62609939/article/details/129493213
https://blog.csdn.net/weixin_45560850/article/details/124868500

标签:get,介绍,ThreadLocal,线程,key,简单,Entry,null
From: https://www.cnblogs.com/linhuaming/p/17321484.html

相关文章

  • Calibre GUI PV 流程介绍(0.8um BCD Process )
    Calibre规则名词解释设计规则检查:DesignRuleCheck,DRC版图&原理图一致性检查:LayoutVersusSchematics,LVS天线效应检查:Antennaeffect,ANT电学规则检查:ElectricalRuleChecking,ERC寄生参数提取:LayoutParameterExtraction,LPE注意1:需要加的CDL文件cdl器件文件放到网......
  • docker:Dockerfile、docker私有仓库、dockercompose介绍、dockercompose部署flask+redi
    目录一、Dockerfile1.1常用和不常用命令1.2dockerfile构建一个djagno项目二、docker私有仓库2.1镜像传到官方仓库2.2镜像分层2.3私有仓库搭建三、dockercompose介绍四、dockercompose部署flask+redis项目4.1新建flask项目app.py4.2编写Dockerfile--》用于构建flask项目的......
  • docker,Dockerfile,docker私有仓库,dockercompose介绍,dockercompose部署flask+redis项目,d
    内容回顾容器操作dockerstart容器id启动容器dockerstop容器id停止容器dockerrm 容器id删除容器ockerrm`dockerps-aq`#正在运行的容器不能删除dockerexec容器id命令让容器执行命令dockercp宿主机目录容器id:容器目录#目录要存在dockercp容......
  • Delphi FDMemTable内存表用法及简单操作函数封装(转)
    在某些场景下当轻量级的应用需要在内存中缓存数量比较多且字段比较多的高频使用数据时。以前我都是采用Ini或直接使用sqlite数据库。JSON也试过基本无法或很难实现需要的功能,因为当涉及某一同类型对象多字段多列时不通过遍历基本无法直接取到或修改数据。这样就导致了效率的低下。......
  • 网络编程-包过滤防火墙简单实现
    一、netfilter框架这次实验使用netfilter框架,参考《网络编程》相关知识以及样例代码。Netfilter是Linux内核中的一个框架,它为以定制处理器形式实施的各种网络相关操作提供了灵活性。Netfilter提供数据包过滤、网络地址翻译和端口翻译的各种选项。检查点在netfilter中,对于IPv......
  • i7cpu温度90度会坏吗详细介绍
    有很多喜欢玩游戏的小伙伴在选择CPU的时候都会选择IntelCorei7处理器,那么这款i7cpu温度90度会坏吗?下面就为大家带来详细介绍。【CPU温度多少正常相关介绍】i7cpu温度90度会坏吗:答:i7cpu温度90度不会坏。i7cpu温度90度不会坏,会损伤到硅晶体的温度大概在115度,只要低于该温度就......
  • 鲁大师能否杀毒详情介绍
    小伙伴们有了电脑之后肯定都会要下载个杀毒软件来保护电脑,鲁大师名气响亮很多的用户就很好奇他是否可以杀毒呢?下面就一起来看看详细的介绍吧。鲁大师能否杀毒:答:不可以。1、鲁大百师是硬件检测和优化工具,是不具有杀毒功能的,建议使用正规度的杀毒软件来保护系统。2、现在主流的......
  • 小米手环8价格介绍
    就在18号小米手环8也会跟很多新品一起发布了,许多用户最为关注的就是这款产品的价格如何,目前来看还没有具体的信息,但是根据之前的消息来看可以猜测出一个大概的价格小米手环8价格介绍答:可能会在300元起售。这个价格也是根据上一代的小米手环7的价格来猜测得出来的,具体如何还是要......
  • 瑞吉外卖项目介绍
    所需知识Java基础知识JavaWebMySQLSpringBootSSM(Spring、SpringMVC、MyBatis)Maven项目介绍瑞吉外卖项目包括两个部分:后台管理系统(内部人员)和移动端(用户)    角色后台系统管理员:登录后台管理系统,拥有后台系统中的所有操作权限后台系统普通员工:登录后台......
  • 使用 Python 的 socket 库来实现一个简单的 Socket 示例
    以下是一个简单的服务器端和客户端的例子:服务器端:pythonimportsocket#创建socket对象serversocket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#获取本地主机名host=socket.gethostname()#设置端口号port=9999#绑定端口号serversocket.bind((h......