目录
- 一、常见场景
- 二、进阶使用
- 三、使用漏洞
- 四、终阶使用
- 总结
一、常见场景
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使用姿势内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!