Java之ThreadLocal使用常见和方式案例讲解

作者:mind_programmonkey 时间:2023-08-28 13:54:09 

【面试高频】- ThreadLocal的使用场景以及使用方式是怎么样的

1 两大使用场景-ThreadLocal的用途

  • 典型场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)

  • 典型场景2:每个线程内需要保存全局变量(例如在 * 中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。

2 典型场景1:每个线程需要一个独享的对象

每个Thread内有自己的实例副本,不共享;

举例:SimpleDateFormat。(当多个线程共用这样一个SimpleDateFormat,但是这个类是不安全的)

  • 2个线程分别用自己的SimpleDateFormat,这没问题;

  • 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅,但勉强可以接受

  • 但是当需求变成了1000,那么必然要用线程池,消耗内存太多;

  • 但是每一个SimpleDateFormat我们都需要创建一遍,那么太耗费new对象了,改成static共用的,所有线程都共用一个simpleDateFormat对象,但这是线程不安全的,容易出现时间一致的情况,在调用的时候,可加锁来解决,但还是不优雅;

  • 用ThreadLocal来解决该问题,给每个线程分配一个simpledateformat,可这个threadlocal是安全的;


package threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* 描述:     利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
*/
public class ThreadLocalNormalUsage05 {

public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

public static void main(String[] args) throws InterruptedException {
       for (int i = 0; i < 1000; i++) {
           int finalI = i;
           threadPool.submit(new Runnable() {
               @Override
               public void run() {
                   String date = new ThreadLocalNormalUsage05().date(finalI);
                   System.out.println(date);
               }
           });
       }
       threadPool.shutdown();
   }

public String date(int seconds) {
       //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
       Date date = new Date(1000 * seconds);
//        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
       SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
       return dateFormat.format(date);
   }
}

class ThreadSafeFormatter {

public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
       @Override
       protected SimpleDateFormat initialValue() {
           return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
       }
   };

public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
           .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

3 典型场景2:当前用户信息需要被线程内所有方法共享

  • 一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),以此类推,但是这样做会导致代码冗余且不易维护。

  • 进阶点就是userMap来保存,但是当多线程同时工作时,需要保证线程安全,需要用synchronized,或者concurrenthashmap,但无论用什么,都会对性能有所影响

每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦

  • 用ThreadLocal保存一些业务内存(用户权限信息,从用户系统获取到的用户名、userId等)

  • 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的

  • 在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象作为参数传递的麻烦


package threadlocal;

/**
* 描述:     演示ThreadLocal用法2:避免传递参数的麻烦
*/
public class ThreadLocalNormalUsage06 {

public static void main(String[] args) {
       new Service1().process("");

}
}

class Service1 {

public void process(String name) {
       User user = new User("超哥");
       UserContextHolder.holder.set(user);
       new Service2().process();
   }
}

class Service2 {

public void process() {
       User user = UserContextHolder.holder.get();
       ThreadSafeFormatter.dateFormatThreadLocal.get();
       System.out.println("Service2拿到用户名:" + user.name);
       new Service3().process();
   }
}

class Service3 {

public void process() {
       User user = UserContextHolder.holder.get();
       System.out.println("Service3拿到用户名:" + user.name);
       UserContextHolder.holder.remove();
   }
}

class UserContextHolder {

public static ThreadLocal<User> holder = new ThreadLocal<>();

}

class User {

String name;

public User(String name) {
       this.name = name;
   }
}

注意点:

  • 强调的是同一个请求内(同一个线程内)不同方法见的共享;

  • 不需重写initialValue()方法,但是必须手动调用set()方法

4 ThreadLocal方法使用总结

场景一:initialValue

在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制。

场景二:set

如果需要保存到ThreadLocal里面的对象的生成时机不由我们随意控制。例如 * 生成的用户信息,用ThreadLocal.set直接放到ThreadLocal当中。

5 ThreadLocal原理

理清Thread,ThreadLocalMap以及ThreadLocal

Java之ThreadLocal使用常见和方式案例讲解

主要方法介绍

  • T initialValue(): 初始化

  • void set(T t): 为这个线程设置一新值

  • T get(): 得到这个线程对应的value。如果是首次调用get()。则会调用initialize来得到这个值

  • void remove(): 删除这个线程得到的值

ThreadLocalMap发生冲突之后,会用线性探测法。

6 ThreadLocal使用问题内存泄露

什么是内存泄露:某个对象不再有用,但是占用的内存却不能被回收。

Value的泄露

  • 在ThreadLocalMap中的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一个对value的强引用。

  • 正常情况 ,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。

  • 但是,如果线程不终止(比如线程池需要保持很久),那么key对应的value就不能被回收。Thread->ThreadLocalMap->Entry(key为Null)->Value

  • 因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能出现OOM;JDK已经考虑到这个问题,所以在set,remove,rehash方法中会扫描key为null,会把value也设置为null,这样value对象就可以被回收了。

  • 但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄露。

如何避免内存泄露呢?

  • 调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以使用完ThreadLocal之后,应该调用remove方法。

7 实际应用场景-在spring中的实例分析

  • DateTimeContextHolder:用到了ThreadLocal

  • RequestContextHolder:用到了ThreadLocal

来源:https://codingchaozhang.blog.csdn.net/article/details/118220731

标签:Java,ThreadLocal
0
投稿

猜你喜欢

  • 设置session有效时间的三种方式

    2022-03-11 15:55:15
  • Java基本数据类型与类型转换实例分析

    2021-07-13 14:41:29
  • Flutter web bridge 通信总结分析详解

    2022-05-23 05:15:42
  • java面试题之数组中的逆序对

    2021-08-05 10:51:02
  • Android游戏开发之黑白棋

    2023-05-24 13:39:03
  • vscode使用官方C/C++插件无法进行代码格式化问题

    2022-07-13 06:24:47
  • Android 图片处理避免出现oom的方法详解

    2023-09-07 07:26:12
  • javaSE基础如何通俗的理解javaBean是什么

    2023-09-20 00:34:13
  • c# 识别图片格式的方法

    2022-07-30 02:14:56
  • 7种形式的Android Dialog使用实例

    2023-02-08 19:41:02
  • Java接入支付宝授权第三方登录的完整步骤

    2023-01-05 01:13:54
  • 详解SpringBoot构建的Web项目如何在服务端校验表单输入

    2021-08-22 23:51:59
  • android 多点触摸图片缩放的具体实现方法

    2023-03-14 05:49:31
  • SpringBoot2 实现JPA分页和排序分页的案例

    2022-02-23 13:26:08
  • SpringBoot如何集成PageHelper分页功能

    2022-10-22 03:18:34
  • 一篇看懂Java中的Unsafe类

    2022-02-13 04:02:38
  • Redisson RedLock红锁加锁实现过程及原理

    2022-12-29 20:49:42
  • Java函数式编程(六):Optional

    2021-07-04 21:44:05
  • Android实现调用摄像头进行拍照功能

    2021-07-16 20:26:07
  • Android编程中的5种数据存储方式

    2022-04-22 07:05:09
  • asp之家 软件编程 m.aspxhome.com