首页 > 编程语言 >Java ThreadLocal 类的使用

Java ThreadLocal 类的使用

时间:2024-05-08 21:14:44浏览次数:24  
标签:Java ThreadLocalMap value ThreadLocal 线程 使用 static public

基于 Java - ThreadLocal 类的使用整理

  • ThreadLocal 表示线程的局部变量,当前线程可以通过 set/get 来对这个局部变量进行操作,其他线程不能对其进行访问

    • ThreadLocal 支持泛型,也就是支持指定 value 类型,像是ThreadLocal<Date>就是指定 value 为 Date 类型。

    • 每个线程会有一份私有的 ThreadLocalMap 变量(threadLocals),去储存这个线程自己想存放的 ThreadLocal 变量们。

      public class Thread implements Runnable {
          //Thread 类里的 threadLocals 存放此线程的专有的 ThreadLocalMap
          ThreadLocal.ThreadLocalMap threadLocals = null;
      }
      

      ThreadLocalMap 可以理解为是一个 Map,Map 的 key 是指向某个 ThreadLocal 对象的引用,value 就是这个线程自己 set 的值。实际上,ThreadLocalMap 中保存有一个 Entry 集合,Entry 中用两个域分别保存 key 和 value。

    • 对于一个线程来说,一个 ThreadLocal 只能关联一个值,而一个线程可以关联多个 ThreadLocal。

      下面有个例子,在 main 线程中的 ThreadLocalMap,就有两个 key-value 的映射,分别是 userIdThreadLocal -> 100、userNameThreadLocal -> hello。

      public class Main {
          public static void main(String[] args){
              ThreadLocal<Integer> userIdThreadLocal = new ThreadLocal<>();
              ThreadLocal<String> userNameThreadLocal = new ThreadLocal<>();
      
              userIdThreadLocal.set(100);
              userNameThreadLocal.set("hello");
          }
      }
      
    • 当调用 ThreadLocal tltl.get()方法时,其实就是先去取得此线程的 ThreadLocalMap,然后再去查找这个 Map 中的 key 为 的tl那个 Entry 的 value 值。

  • ThreadLocal 常用的方法

    • set(x): 设置此线程局部变量的想要放的值是多少[1]

    • get(): 取得此线程局部变量当初存放的值,如果没有存放过则返回 null;

    • remove(): 删除此线程局部变量的键值对,也就是如果先执行 remove 再执行 get,会返回 null。

  • ThreadLocal 可以用在 SimpleDateFormat,或是 SpringMVC 上

    • 因为 SimpleDateFormat 不是线程安全的,虽然可以每次要使用的时候重新new一个,但是这样做会很浪费资源,所以如果使用 ThreadLocal 在每个线程里都存放一个此线程专用的 SimpleDateFormat,就可以避免一直new的资源浪费,同时又确保线程安全。

    • 因为 SpringMVC 会对每个请求分配一个线程,可以在拦截器将此线程的用户信息(ip、名字…)使用 ThreadLocal 储存,这样在后续要用到用户信息的地方时,就可以去 ThreadLocal 中取得,而且因为 ThreadLocal 可以隔离线程,因此每条请求对应的线程的用户信息不会互相干扰。

  • ThreadLocal 可能造成的内存泄漏

    之所以 ThreadLocal 会发生内存泄漏,原因是因为只要线程活着,这个线程的 ThreadLocalMap 就会一直活着,而当初透过 ThreadLocal set() 的值,也就会在 ThreadLocalMap 中一直存在,也就是该 ThreadLocal 和该 value 的内存地址始终都有这个 ThreadLocalMap 在引用着(被 ThreadLocalMap 中的 Entry 直接引用),导致 GC 无法回收它,所以才会发生内存泄漏。

    为了解决这个问题,Java 做了一个小优化,在 ThreadLocalMap 中使用弱引用来指向 ThreadLocal,如果一个 ThreadLocal 没有外部强引用来引用它,只有这条 ThreadLocalMap 的弱引用来引用它时,那么当系统 GC 时,这些 ThreadLocal 就会被回收(因为是弱引用),如此一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry 们。

    public class ThreadLocal<T> {
        //根据线程,取得那个线程自己的 ThreadLocalMap
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
        static class ThreadLocalMap {
            //ThreadLocalMap 的 key 是使用“弱引用”的 ThreadLocal
            static class Entry extends WeakReference<ThreadLocal> {
                Object value;
    
                //ThreadLocalMap 中的 key 就是 ThreadLocal,value 就是设置的值
                Entry(ThreadLocal k, Object v) {
                    super(k);
                    value = v;
                }
            }
        }
    }
    

    下图中,实线表示强引用,虚线表示弱引用:

    img

    这个弱引用优化只能使得 ThreadLocal 被正确回收,但是这些 key 为 null 的 Entry 们仍然会存在在 ThreadLocalMap 里,因此 value 仍然无法被回收。

    所以 Java 又做了一个优化,就是在 ThreadLocal 执行get()set()remove()方法时,都会将该线程 ThreadLocalMap 里所有 key = null 的 value 也设置为 null,手动帮助 GC:

    ThreadLocal k = e.get();
    if (k == null) {
        e.value = null; // Help the GC
    } 
    

    但是根本上的解决办法,还是在当前线程使用完这个 ThreadLocal 时,就及时remove()掉该 value,也就是使得 ThreadLocalMap 中不要存在这个键值对,这样才能确保 GC 能正确回收。

  • 具体实例

    • 每个线程都可以在 ThreadLocal 中放自己的值,且不会干扰到其他线程的值

      class Tools {
          public static ThreadLocal threadLocal = new ThreadLocal();
      }
      
      class MyThread extends Thread {
          @Override
          public void run() {
              if (Tools.threadLocal.get() == null) {
                  Tools.threadLocal.set(Thread.currentThread().getName() + ", " + Math.random());
              }
              System.out.println(Tools.threadLocal.get());
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              for (int i = 0; i < 5; i++) {
                  MyThread thread = new MyThread();
                  thread.setName("thread " + i);
                  thread.start();
              }
          }
      }
      
      thread 1, 0.86
      thread 0, 0.42
      thread 2, 0.35
      thread 3, 0.41
      thread 4, 0.45
      

      可以看到,不同线程的 ThreadLocalMap 中的 key 可能指向同一个 ThreadLocal 对象,也就是 ThreadLocal 可以是多线程共享的,但是 key 对应的 value 因为保存在每个线程单独的 ThreadLocalMap 中,而无法多线程共享。

      注意:这里仅仅是为了演示而使用 public static 修饰 ThreadLocal,这会产生一个指向 ThreadLocal 的强引用,可能导致内存泄漏。

    • 使用 ThreadLocal 在 SimpleDateFormat 上,并且给 ThreadLocal 加上泛型,指定 value 的类型是 SimpleDateFormat

      因为使用了 ThreadLocal 确保每个线程有自己一份 SimpleDateFormat,所以线程安全,不会报错。

      class MyThread extends Thread {
          private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>();
          
          @Override
          public void run() {
              SimpleDateFormat sdf = threadLocal.get();
              if (sdf == null) {
                  sdf = new SimpleDateFormat("yyyy-MM-dd");
                  threadLocal.set(sdf);
              }
              try {
                  System.out.println(sdf.parse("2018-07-15"));
              } catch (ParseException e) {
                  System.out.println("报錯了");
              }
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              for (int i = 0; i < 5; i++) {
                  MyThread thread = new MyThread();
                  thread.setName("thread " + i);
                  thread.start();
              }
          }
      }
      
      Sun Jul 15 00:00:00 CST 2018
      Sun Jul 15 00:00:00 CST 2018
      Sun Jul 15 00:00:00 CST 2018
      Sun Jul 15 00:00:00 CST 2018
      Sun Jul 15 00:00:00 CST 2018
      

      另:注意到示例中使用 private static 修饰 ThreadLocal,这通常是受鼓励的做法,private 是为了封装,static 是为了避免重复创建 TSO(Thread Specific Object,即与线程相关的变量)。没有被 static 修饰的 ThreadLocal 变量实例,会随着所在的类多次创建而被多次实例化,这样频繁地创建变量实例没有必要。

    • 使用 ThreadLocal 在 SpringMVC 上

      • 拦截器 MyInterceptor 先去从 cookie 中取得当前用户信息,透过 UserUtils 放到ThreadLocal<User>

      • 然后当 MyController 要去取得这个请求(也就是这条线程)的用户信息时,就去调用 UserUtils 取得放在ThreadLocal<User>里面的 User 信息

      • 最后当请求结束时,删除此条线程的ThreadLocal<User>信息,避免内存泄漏

        //UserUtils 专门存取 User 信息
        public class UserUtils {
            public static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
        
            public static void setUser(User user) {
                userThreadLocal.set(user);
            }
        
            public static User getUser() {
                return userThreadLocal.get();
            }
        
            public static void removeUser() {
                if (userThreadLocal.get() != null) {
                    userThreadLocal.remove();
                }
            }
        }
        
        //拦截器取得 cookie 中的 User 信息,并调用 UserUtils 放到 ThreadLocal 里
        //请求结束时要记得把 ThreadLocal 中的 User 刪除,因为这条线程之后还要去服务其他请求
        public class MyInterceptor extends HandlerInterceptorAdapter {
            @Override
            public boolean preHandle() throws Exception {
                User user = getUserFromCookie();
                UserUtils.setUser(user);
                return true;
            }
        
            @Override
            public void postHandle() throws Exception {
                UserUtils.removeUser();
            }
        }
        
        //MyController 调用 UserUtils 取得 ThreadLocal<User> 中的 User
        @Controller
        @RequestMapping("/")
        public class MyController {
            @RequestMapping("/")
            public void test() {
                User user = UserUtils.getUser();
                System.out.println("User id: " + user.id);
            }
        }
        

参考:

ThreadLocal 的 Entry 为什么要继承 WeakReference?

ThreadLocal 原理及使用场景

为何通常“将 ThreadLocal 变量设置为 static”?

将 ThreadLocal 变量设置为 private static 的好处是啥?


  1. 注意:从 API 上看,似乎是将变量值保存在了 ThreadLocal 中,但是从源码上看,变量值实际上保存在了 ThreadLocalMap 中或者说 Thread 中。下面看到类似于“在 ThreadLocal 中放值”这样的说法时,应该意识到在源码层面上的另一种解释。 ↩︎

标签:Java,ThreadLocalMap,value,ThreadLocal,线程,使用,static,public
From: https://www.cnblogs.com/Higurashi-kagome/p/17961851

相关文章

  • 团队共同完成——谈谈本小组项目的测试1、你希望不同的人物角色(有些团队有3个人物角色
    ]团队共同完成——谈谈本小组项目的测试1、你希望不同的人物角色(有些团队有3个人物角色)如何使用你的软件?他们的需求和目标是什么,您的功能是如何协同工作来解决他们的需求的?2、本小组项目的测试矩阵是什么?是在什么平台、什么语言、什么类型的机器、什么类型浏览器等上测试您的......
  • 银弹:为了避免项目的成员为了一些问题争执不休,公司发明了银弹(Silver Bullet)这一工具。
    我的答案:【第二组】答:银弹作为一种工具,其目的是为了在项目团队中解决争议,确保项目能够继续前进。然而,是否真的有用,需要从多个角度进行考量:团队沟通与协作:银弹提供了一种强制性的解决方案,可以在一定程度上减少无休止的争论,促使团队成员快速做出决策。这在某些情况下可以提......
  • Java容器化改造
    dockerjava项目容器化改造前后端分离项目前端https://gitee.com/yuco/eladmin-web.git后端https://gitee.com/yuco/eladmin.git要素:vuenpmspringbootmysqlredisjava后端容器化思路:了解在物理机虚拟机的部署流程,然后编写dockerfile进行容器化部署。java项目,使用mv......
  • 个人练习(每个同学都要提交)——学习和使用多个平台上的测试工具 请使用两种以上平台上
    答:在"校园跑腿"项目中,我们可以使用两种不同的测试工具来确保软件的质量和稳定性。这些测试工具可以包括自动化测试工具和手动测试工具。以下是两种测试工具的介绍以及如何在项目中使用它们:SeleniumWebDriver(自动化测试工具):SeleniumWebDriver是一个流行的自动化测试工具,用于测......
  • 个人练习(每个同学都要提交)——学习和使用多个平台上的测试工具 请使用两种以上平台上
    在本小组项目中,我们使用以下两种平台上的测试工具:Postman和Swagger。Postman:Postman是一款功能强大的API测试工具,可以用于创建、调试和测试API。首先,我们需要创建一个Postman账号,并安装Postman应用程序。在项目中,我们可以使用Postman来进行API端点的测试。我们可以创建多个请求......
  • 对接诺诺电子发票(Java)
    沙箱环境:url:https://sandbox.nuonuocs.cn/open/v1/servicesappKey:SD63236305appSecret:SDDED2523BED4643下载诺诺的SDK:SDK下载引入项目: 服务实现层代码://创建发票信息表@OverridepublicInvoiceDtocreate(Invoiceresources){resources.setId(snowflake.......
  • qt 使用windows 的USB库 hid.dll 报错 :undefined reference to `HidD_GetAttributes(v
     1、cmakeLists.txt文件引入相应的库: cmake_minimum_required(VERSION3.5)project(test1LANGUAGESCXX)file(GLOBSRC./*.cpp./*.h)add_executable(test1${SRC})include(GNUInstallDirs)target_link_libraries(${PROJECT_NAME}hidsetupapi)inst......
  • Java护照识别接口开发示例、文字识别、证件识别
    护照是我们出国旅行时所必要的证件之一,他是我国公民去外国的旅行和工作的时候所代表的一个合法的身份证件。在护照上面也有不少关于我们个人身份的信息,而手动去录入如此多的身份信息这绝对是灾难。不仅证件,有的场景还需要录入很多文字信息。翔云API可识别图片上的身份证、护......
  • Java实名认证API、婚恋网实名认证
    中国网络婚恋交友行业发展近20年,电脑端网络婚恋服务已经较为成熟,商业模式也较为完善。但随着移动互联网的快速发展,移动端成为婚恋交友企业核心用户新的来源渠道。网络婚恋交友移动端人群覆盖规模逐渐超过电脑端人群,标志着以移动端为主导的婚恋交友服务正式来临,整体行业迎来了......
  • javaScript之局部变量,全局变量与局部作用域,全局作用域
    前端开发工作者,最需要学习的一门语言就是JavaScript了吧,其实学习大部分编程语言都是从基本的语法知识开始人门的。什么语句、变量、数据类型、对象、函数...今天本文就简单说明javaScript变量中的一个小小的知识点,其实在后面的最开始工作编程中我也是常常容易出现bug的一个点。......