惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
小众软件
小众软件
博客园_首页
博客园 - 聂微东
V
V2EX
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
SegmentFault 最新的问题
J
Java Code Geeks
Last Week in AI
Last Week in AI
The Cloudflare Blog
月光博客
月光博客
雷峰网
雷峰网
宝玉的分享
宝玉的分享
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
博客园 - Franky
腾讯CDC
Jina AI
Jina AI
博客园 - 叶小钗
大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
量子位
爱范儿
爱范儿
美团技术团队
T
Tailwind CSS Blog
博客园 - 【当耐特】
D
Docker
IT之家
IT之家
V
Visual Studio Blog
P
Proofpoint News Feed
L
LangChain Blog
Engineering at Meta
Engineering at Meta
C
Check Point Blog
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
Recorded Future
Recorded Future

CAYZLH

MacBook上备忘录APP的快捷键和手势 让Google屏蔽某些搜索结果 WSL自定义安装Ubuntu typora自动上传图片配置 自建giscus服务 Windows下结束指定端口的进程 批量修改maven多模块版本号 使用winsw部署SpringBoot项目 Gson简易指南 docker搭建Consul集群 使用Docker部署SpringBoot项目 Docker使用redis镜像 Docker使用rabbitmq Docker使用zookeeper Docker使用MySQL Dockerfile常用指令 Docker免sudo操作 雷鸟电视去广告 使用ADB卸载MIUI系统应用 利用GitHub做图床 将网站变成灰色 Github上传大文件 Android远程调试命令adb vscode快捷键的使用 SpringBoot文件上传异常处理 SpringBoot封装JedisUtils工具类 SpringBoot自动部署脚本 使用Spring读取文件的几种方式 Spring统一异常返回 利用Github做Maven私服 Redis常用指令 Maven常用指令 使用mysqldump导出数据 SpringBoot动态切换多数据源 Java8的日期处理实践 lambda表达式语法 Stream表达式语法 使用Optional优雅地判空 Android Support vs AndroidX 「Vue」Runtime-Complier和Runtime-only的区别 RecyclerView使用记录 动态设置布局之LayoutInflater Redis分布式锁的几种方案 打造一个舒服的写作环境(Hexo) Maven中dependencyManagement的作用 Redis配置认证密码 nginx简单配置示例 Linux(macOS)换源 Linux搭建Git服务器 Linux配置ssh使用公钥登录远程服务器 Linux下find与exec的使用 Linux排查Java问题工具单 Linux常用命令 MySql数据库优化细节 API签名验证方案 Java集合框架 Redis基础知识总结 Spring事务管理 Redis为什么这么快 SpringBoot实现Jwt单点登录 SpringBoot实现Redis分布式锁 Spring面向切面编程(知识梳理) SpringBoot异步请求和异步调用 Mybatis中用到的几种设计模式 Docker Swarm RocketMQ相关流程图/原理图 SonarQube的安装与使用 宝塔面板部署vue项目 Tomcat的三种接收请求处理方式 Docker知识扫盲 安装Jenkins并用于部署SpringBoot项目 Tomcat单机多实例部署 Maven中Scope的分类 Springboot集成Shiro(前后端分离) Linux搭建frp服务(内网穿透) Redis高逼格指令 Docker私有仓库的搭建与使用 Dockerfile使用介绍 Redis的使用场景 Docker的网络模式
深入了解与使用ThreadLocal
Ant丶 · 2021-04-28 · via CAYZLH

什么是ThreadLocal

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

For example, the class below generates unique identifiers local to each thread. A thread’s id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.

ThreadLocal提供了线程的局部变量,每个线程都可以通过set()get()方法操作此变量,并且不会与其他线程的局部变量进行冲突,保证了线程间局部变量的隔离行。

翻译一下:ThrealLocal类型的变量属于当前线程,哪怕是同一个方法内的同一个ThreadLocal变量,他们的值在不同的业务运行下也是不一样的,线程安全。

ThreadLocal业务应用场景

数据库连接

jdbc时代,数据库连接connection需要我们手动来维护,同一个库连接我们可以用数据库连接池来实现。但是不同数据库连接的时候,不同的线程需要获取到不同的连接。这时候就可以使用ThreadLocal来维护线程池中不同数据源连接。

示例代码:

来源:https://blog.csdn.net/u013360850/article/details/78861442
public class DynamicDataSourceContextHolder {

private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);




private static Lock lock = new ReentrantLock();




private static int counter = 0;




private static final ThreadLocal<Object> CONTEXT_HOLDER = ThreadLocal.withInitial(DataSourceKey.master);





public static List<Object> dataSourceKeys = new ArrayList<>();




public static List<Object> slaveDataSourceKeys = new ArrayList<>();






public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}




public static void useMasterDataSource() {
CONTEXT_HOLDER.set(DataSourceKey.master);
}




public static void useSlaveDataSource() {
lock.lock();

try {
int datasourceKeyIndex = counter % slaveDataSourceKeys.size();
CONTEXT_HOLDER.set(String.valueOf(slaveDataSourceKeys.get(datasourceKeyIndex)));
counter++;
} catch (Exception e) {
logger.error("Switch slave datasource failed, error message is {}", e.getMessage());
useMasterDataSource();
e.printStackTrace();
} finally {
lock.unlock();
}
}






public static String getDataSourceKey() {
return CONTEXT_HOLDER.get();
}




public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}







public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
}

以上代码用于数据源上下文配置,用于切换数据源。

全局变量传递

最典型的例子就是用户数据的传递,请求的时候通过Header将用户的token传递,通过拦截器(过滤器、AOP)等方式将token放到ThreadLocal,让当前线程的相关方法共享该变量,减少参数传递。

示意图:

image-20210428223107168

示例代码:

public class ContextHolder {

private static final ThreadLocal<AccountLoginInfoBo> ACCOUNT_LOGIN_INFO_HOLDER = new ThreadLocal<>();

private static final ThreadLocal<String> REQUEST_ID_HOLDER = new ThreadLocal<>();

public static AccountLoginInfoBo getAccountLoginInfo() {
return ACCOUNT_LOGIN_INFO_HOLDER.get();
}

}

链路追踪

微服务架构下,多个服务之间调用如果出现了报错,使用链路追踪是一个常见的手段。在请求的线程变量中嵌入traceId,根据这个traceId就可以找到对应请求分别在各个业务应用的日志。

示例代码:

public class GeneralInterceptorHandler extends HandlerInterceptorAdapter {

@Override
public boolean preHandle(HttpServletRequest request,
javax.servlet.http.HttpServletResponse response, Object handler) throws Exception {
request.setAttribute("_timestamp", System.currentTimeMillis());

String requestId = RandomStringUtils.randomNumeric(10);
MDC.put("requestId", requestId);

BaseContextHolder.setRequestId(requestId);
String requestUri = request.getRequestURI();
BaseContextHolder.setRequestUrl(requestUri);
log.info("request uri: {}", requestUri);

return super.preHandle(request, response, handler);
}

@Override
public void postHandle(HttpServletRequest request,
javax.servlet.http.HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {

long begin = (long) request.getAttribute("_timestamp");
long end = System.currentTimeMillis();
log.info("process success. cost {}ms. ", end - begin);
MDC.remove("requestId");
BaseContextHolder.remove();
super.postHandle(request, response, handler, modelAndView);
}
}

示例结果:

image-20210428223736918

在以上代码中,除了将生成的请求ID放到MDC中,还将其放入到Context类中定义的ThreadLocal中,方便在代码中直接拿出来使用,这样通过日志就能打印出每次的请求ID,在微服务请求链路中查找日志只需要使用cat xxx.log | rep 8475913673就可以找到本次请求的所有链路日志。

ThreadLocal原理解析

顺便了解一下ThreadLocal原理,看一下set()get()remove()方法的实现。

set()方法










public void set(T value) {

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

if (map != null)
map.set(this, value);

else
createMap(t, value);
}

查看源码发现set()方法中使用到了ThreadLocalMap类。

展开查看ThreadLocalMap类源码
static class ThreadLocalMap {









static class Entry extends WeakReference<ThreadLocal<?>> {

Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}




private static final int INITIAL_CAPACITY = 16;





private Entry[] table;




private int size = 0;




private int threshold;




private void setThreshold(int len) {
threshold = len * 2 / 3;
}




private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}




private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}






ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}







private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}











private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}










private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}







private void set(ThreadLocal<?> key, Object value) {






Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}




private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
















private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;





int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;



for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();






if (k == key) {
e.value = value;

tab[i] = tab[staleSlot];
tab[staleSlot] = e;


if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}




if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}


tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);


if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}












private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;


tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;


Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;



while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

























private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}






private void rehash() {
expungeStaleEntries();


if (size >= threshold - threshold / 4)
resize();
}




private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;

for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}




private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
}

其中维护了一个entry结构用来用来维护节点的数据,细心地同学应该已经发现了Entry这个结构继承了WeakReference,从构造方法可以看出,ThreadLocalMapKey是软引用维护的。

ThreadLocal.ThreadLocalMap threadLocals = null;

上面这行代码是一个成员变量:*针对于每一个线程,都是独立维护一个ThreadLocalMap,一个线程也可以拥有多个ThreadLocal变量。*

get()方法

get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
setInitialValue()
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
initialValue()
protected T initialValue() {
return null;
}

get()方法整体上比较简单,贴上了关键逻辑逻辑代码,调用get()时,如果存在值,则将值返回,不存在值调用setInitialValue()获取值,其中初始化的值为null,也就是说如果ThreadLocal变量未被赋值,或者赋值后被remove掉了,直接调用get()方法不会报错,将会返回null值。

remove()方法

ThreadLocal#remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap#remove
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

remove方法调用时会判断当前线程中ThreadLocalMap是否存在,如果存在则调用ThreadLocalMap.remove(key);遍历链表结构移除entry节点。

小结

  • Thread维护着一个ThreadLocalMap的引用,其中ThreadLocalMapkeyWeakReference维护。
  • ThreadLocal本身并不存储值,ThreadLocal通过操作ThreadLocalMap达到对线程变量的赋值,获取,删除操作。

ThreadLocal内存泄漏问题

JVM中对ThreadLocal的堆栈维护图:

image-20210426152220803.png

(图片来源见水印)

entry对于value的引用为强应用,key的引用为弱引用。

如果一个对象只是被弱引用引用者,那么只要发生 GC,不管内存空间是否足够,都会回收该对象。

那么问题就来了,如果操作ThreadLocal变量的方法QPS很高,疯狂被请求,这个时候调用了set(),get()方法,并未调用remove方法,那么,当GC发生。entryThreadLocal的关联关系中断,Key被回收,value还被强连接关联着。这样跟垃圾回收可达性分析,value仍旧为可达,但是从业务角度上看,这个value值将永远访问不到,出现了内存泄露。

因此在使用ThreadLocal时必须要显示的调用remove方法,否则出现了问题,排查起来都很麻烦。

  • 可以在拦截器、AOP、过滤器结束的时候调用remove方法

线程池中线程上下文丢失

ThreadLocal不能在父子线程中传递,因此最常见的做法是把父线程中的ThreadLocal值拷贝到子线程中,因此大家会经常看到类似下面的这段代码:

for(value in valueList){
Future<?> taskResult = threadPool.submit(new BizTask(ContextHolder.get()));
results.add(taskResult);
}
for(result in results){
result.get();
}

提交的任务定义长这样:

class BizTask<T> implements Callable<T>  {
private String session = null;

public BizTask(String session) {
this.session = session;
}

@Override
public T call(){
try {
ContextHolder.set(this.session);

} catch(Exception e){

} finally {
ContextHolder.remove();
}
return null;
}
}

对应的线程上下文管理类:

class ContextHolder {
private static ThreadLocal<String> localThreadCache = new ThreadLocal<>();

public static void set(String cacheValue) {
localThreadCache.set(cacheValue);
}

public static String get() {
return localThreadCache.get();
}

public static void remove() {
localThreadCache.remove();
}

}

线程池的设置:

ThreadPoolExecutor executorPool 
= new ThreadPoolExecutor(20, 40, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(40),
new XXXThreadFactory(), ThreadPoolExecutor.CallerRunsPolicy);

其中最后一个参数控制着当线程池满时,该如何处理提交的任务,内置有4种策略:

ThreadPoolExecutor.AbortPolicy 
ThreadPoolExecutor.DiscardPolicy
ThreadPoolExecutor.DiscardOldestPolicy
ThreadPoolExecutor.CallerRunsPolicy

可以看到,我们初始化线程池的时候指定如果线程池满,则新提交的任务转为串行执行,那我们之前的写法就会有问题了,串行执行的时候调用ContextHolder.remove();会将主线程的上下文也清理,即使后面线程池继续并行工作,传给子线程的上下文也已经是null了,而且这样的问题很难在预发测试的时候发现。

并行流中线程上下文丢失

如果ThreadLocal碰到并行流,也会有很多有意思的事情发生,比如有下面的代码:

class ParallelProcessor<T> {

public void process(List<T> dataList) {

dataList.parallelStream().forEach(entry -> {
doIt();
});
}

private void doIt() {
String session = ContextHolder.get();

}
}

这段代码很容易在线下测试的过程中发现不能按照预期工作,因为并行流底层的实现也是一个ForkJoin线程池,既然是线程池,那ContextHolder.get()可能取出来的就是一个null。我们顺着这个思路把代码再改一下:\

class ParallelProcessor<T> {

private String session;

public ParallelProcessor(String session) {
this.session = session;
}

public void process(List<T> dataList) {

dataList.parallelStream().forEach(entry -> {
try {
ContextHolder.set(session);

doIt();
} catch (Exception e) {

} finally {
ContextHolder.remove();
}
});
}

private void doIt() {
String session = ContextHolder.get();

}
}

修改完后的这段代码可以工作吗?如果运气好,你会发现这样改又有问题,运气不好,这段代码在线下运行良好,这段代码就顺利上线了。不久你就会发现系统中会有一些其他很诡异的bug。原因在于并行流的设计比较特殊,父线程也有可能参与到并行流线程池的调度,那如果上面的process方法被父线程执行,那么父线程的上下文会被清理。导致后续拷贝到子线程的上下文都为null,同样产生丢失上下文的问题。