我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

一、常见场景

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堆积在内存中,逐步变多,最终撑爆内存。

 在实际的业务场景中,需要传递的可能有订单号,交易号,流水号等等,这些变量往往是唯一不重复的、符合案例中的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();
}

总结

到此这篇关于ThreadLocal使用姿势的文章就介绍到这了,更多相关ThreadLocal使用姿势内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

VUE 组件:前端开发中程序员最喜欢的玩具

前端开发中程序员的最爱:Vue 组件
VUE 组件:前端开发中程序员最喜欢的玩具
2024-02-28

如何在Linux上运行你最喜欢的Windows应用程序

如何在Linux上运行你最喜欢的Windows应用程序,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。WINE 是一个开源项目,它可以协助很多 Windows 应用程序在 L
2023-06-15

Python工程师最喜欢使用的数据可视化工具有哪些

这篇文章将为大家详细讲解有关Python工程师最喜欢使用的数据可视化工具有哪些,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1、灯果数据可视化(http://www.dengguobi.com/)灯果数据
2023-06-01

Linux下如何安装使用最适合程序员的编程字体

小编给大家分享一下Linux下如何安装使用最适合程序员的编程字体,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!JetBrains Mono 是 JetBrains
2023-06-15

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录