程序员最喜欢的ThreadLocal使用姿势

作者:qiaoHaoTing 时间:2022-10-22 21:14:55 

一、常见场景

1、ThreadLocal作为线程上下文副本,那么一种最常见的使用方式就是用来方法隐式传参,通过提供的set()和get()两个public方法来实现在不同的方法中的参数传递。对于编程规范来说,方法定义的时候是对参数个数是有限制的,甚至在一些大厂,对方法参数个数是有明确规定的。

2、线程安全,每个线程维持自己的变量,以免紊乱,像常用的数据库的连接池的线程安全实现就使用了ThreadLocal。

二、进阶使用

以参数传递为例子,如何更好地使用ThreadLocal来实现在同一线程栈中不同方法中的参数传递。在参数传递的时候,那么都会有参数名,参数值,而ThreadLocal提供的get()和set()方法,不能直接满足设置参数名和参数值。这种情况下就需要对ThreadLocal进一次封装,如下代码,维护一个map对象,然后提供setValue(key, value)和getValue(key, value)方法,就可以很方便地实现了参数的设置和获取;在需要的地方对参数进行清理,使用remove(key)或者clear()即可实现。

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

public class ThreadLocalManger<T> extends ThreadLocal<T> {

private static ThreadLocalManger<Map<String, Object>> MANGER = new ThreadLocalManger<>();

private static HashMap<String, Object> MANGER_MAP = new HashMap<>();

public static void setValue(String key, Object value) {
       Map<String, Object> context = MANGER.get();
       if(context == null) {
           synchronized (MANGER_MAP) {
               if(context == null) {
                   context = new HashMap<>();
                   MANGER.set(context);
               }
           }
       }
       context.put(key, value);
   }

public static Object getValue(String key) {
       Map<String, Object> context = MANGER.get();
       if(context != null) {
           return context.get(key);
       }
       return null;
   }

public static void remove(String key) {
       Map<String, Object> context = MANGER.get();
       if(context != null) {
           context.remove(key);
       }
   }

public static void clear() {
       Map<String, Object> context = MANGER.get();
       if(context != null) {
           context.clear();
       }
   }
}

三、使用漏洞

继续以参数传递为例子,来看看ThreadLocal使用过程中存在的问题和后果。在实际业务的功能开发中,为了提升效率,大部分情况下都会使用线程池来实现,比如数据库的连接池、RPC请求连接池、MQ消息处理池、后台批量job池等等;同时也可能会使用一个伴随整个应用生命周期的线程(守护线程)来实现的一些功能,比如说心跳、监控等等。使用线程池,那么在实际生产业务中并发肯定不低,池中线程就会一直复用;守护线程一旦创建,那么就会活到应用停机。所以在这些情况下,线程的生命周期很长,在使用ThreadLocal的时候,一定要进行清理,不然就会有内存溢出的情况发生。通过以下案例来模拟内存溢出的情况。

通过一个死循环来模拟高并发场景。创建一个10个核心线程数,10个最大线程数数,60秒空闲时间的、线程名以ThreadLocal-demo-开头的线程池,在该场景下,将有10个线程来运行,运行内容很简单:生成一个UUID,并将其作为参数key,然后设置到线程副本中。

import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.*;

@Service
public class ThreadLocalService {

ThreadFactory springThreadFactory = new CustomizableThreadFactory("TheadLocal-demo-");

ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60,
           TimeUnit.SECONDS, new LinkedBlockingQueue<>(), springThreadFactory);

ExecutorService service = new ThreadPoolExecutor(10, 10, 60,
           TimeUnit.SECONDS, new LinkedBlockingQueue<>());

public Object setValue() {
       for(; ;) {
           try {
               Runnable runnable = new Runnable() {
                   @Override
                   public void run() {
                       String id = UUID.randomUUID().toString();
                       // add
                       ThreadLocalManger.setValue(id, "this is a value");
                       //do something here
                       ThreadLocalManger.getValue(id);
                       // clear()
                       //ThreadLocalManger.clear();
                   }
               };
               executorService.submit(runnable);
           } catch (Exception e) {
               e.printStackTrace();
               break;
           }
       }
       return "success";
   }

}

以上代码中已把clear()方法注释掉,不做清理,触发程序,稍微将jvm设置低一些,跑不久就会报如下OOM。

java.lang.OutOfMemoryError: GC overhead limit exceeded
Exception in thread "TheadLocal-demo-9"
Exception in thread "TheadLocal-demo-8"
Exception in thread "TheadLocal-demo-6"
Exception in thread "TheadLocal-demo-10"
Exception in thread "TheadLocal-demo-7"
java.lang.OutOfMemoryError: GC overhead limit exceeded
Exception in thread "TheadLocal-demo-5"
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
   at com.intellij.rt.debugger.agent.CaptureStorage.insertEnter(CaptureStorage.java:57)
   at java.util.concurrent.FutureTask.run(FutureTask.java)
   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
   at java.lang.Thread.run(Thread.java:748)
java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded

就会发生严重的内存溢出,通过如下debug截图可知,设置进去的UUID堆积在内存中,逐步变多,最终撑爆内存。

程序员最喜欢的ThreadLocal使用姿势

 在实际的业务场景中,需要传递的可能有订单号,交易号,流水号等等,这些变量往往是唯一不重复的、符合案例中的UUID情况,在不清理的情况下就会造成应用OOM,进而不可用;在分布式系统中,还能导致上下游系统不可用,进而导致整个分布式进去的不可用;如果这些信息往往还可能用在网络传输中,大消息占有网络带宽,严重甚至导致网络瘫痪。所以一个小小的细节就会置整个集群于危险之中,那么如何合理化解呢。

四、终阶使用

以上问题在于忘记清理,那么如何让清理无感知,即不需要清理也没有问题。根因在于线程跑完一次之后,没有进行清理,所以可提供一个基类线程,在线程执行最后对清理进行封装。如下代码。提供一个BaseRunnable抽象基类,该类主要如下特点。

        1、该类继承Runnable。

        2、实现setArg(key, value)和getArg(key)两个方法。

        2、在重写的run方式中分为两步,第一步,调用抽象方法task;第二步,清理线程副本。

有了以上3个特点,继承了BaseRunnable的线程类,只需要在实现task方法,在task方法中实现业务逻辑,参数传递和获取通过setArg(key, value)和getArg(key)两个方法即可实现,无需再显示清理。

public abstract class BaseRunnable implements Runnable {

@Override
   public void run() {
       try {
           task();
       } finally {
           ThreadLocalManger.clear();
       }
   }

public void setArg(String key, String value) {
       ThreadLocalManger.setValue(key, value);
   }

public Object getArg(String key) {
       return ThreadLocalManger.getValue(key);
   }

public abstract void task();
}

来源:https://blog.csdn.net/qq_34485626/article/details/122522129

标签:threadlocal,线程
0
投稿

猜你喜欢

  • springboot整合mybatis plus与druid详情

    2022-07-31 14:22:14
  • 教你怎么在IDEA中创建java多模块项目

    2023-05-28 19:25:58
  • C# 线程同步详解

    2021-12-30 04:50:03
  • Java编程二项分布的递归和非递归实现代码实例

    2023-08-07 09:38:04
  • 使用Spring Boot 2.x构建Web服务的详细代码

    2022-09-17 04:08:40
  • Java @RequestMapping注解功能使用详解

    2022-08-15 11:06:10
  • Android获取经纬度计算距离介绍

    2022-04-30 20:00:04
  • 关于Springboot中JSCH的使用及说明

    2023-11-28 02:32:16
  • C#同步和异步调用方法实例

    2022-09-11 21:20:50
  • 3种C# 加载Word的方法

    2021-06-05 21:06:41
  • opencv利用视频的前n帧求平均图像

    2021-06-20 11:43:02
  • Java中的访问修饰符详细解析

    2022-01-18 17:23:02
  • Android 自定义Switch开关按钮的样式实例详解

    2023-09-09 16:38:39
  • IDEA中的maven没有dependencies解决方案

    2021-08-01 11:58:50
  • C# 构造函数如何调用虚方法

    2023-05-12 00:08:57
  • Java利用HttpClient模拟POST表单操作应用及注意事项

    2023-11-29 23:48:01
  • SpringBoot Java后端实现okhttp3超时设置的方法实例

    2022-11-06 04:56:03
  • unity3d发布apk在android虚拟机中运行的详细步骤(unity3d导出android apk)

    2022-11-09 16:18:56
  • java实现纸牌游戏之小猫钓鱼算法

    2021-08-11 22:57:00
  • Java窗口精细全方位讲解

    2023-03-05 15:35:15
  • asp之家 软件编程 m.aspxhome.com