1、概览

本文我们来看下java.lang包中的ThreadLocal,它赋予我们给每个线程存储自己数据的能力。

2、ThreadLocal API

ThreadLocal允许我们存储的数据只能被特定的线程``访问

我们现在存储一个整形并把它和一个特定的线程绑定:

ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

接下来当我们在某个线程中想使用这个值的时候,我们只需要调用get()或set()方法,简单的说,我们可以把ThreadLocal理解成数据都存在一个map中,使用线程对象作为key。

当我们在当前线程中调用threadLocalValue的get()方法时,我们能拿到整形值1:

threadLocalValue.set(1);
Integer result = threadLocalValue.get();

我们可以使用ThreadLocal的withInitial()方法并传入一个supplier来创建一个实例:

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

想要删除这个值的时候我们只需要调用一下remove()方法就好了

threadLocal.remove();

在叙述怎么合适的使用ThreadLocal之前我们先来看一个不用ThreadLocal的例子,然后我们再修改例子比较一下。

3、存储用户数据在ConcurentHashMap中

有这么一个程序需要给每一个用户ID存储对应的用户上下文信息:

public class Context {
private String userName; public Context(String userName) {
this.userName = userName;
}
}

我们给每个用户新起一个线程,创建了一个实现了Runnable接口的SharedMapWithUserContext类,run()方法中的UserRepository会查询数据库返回传入用户ID的用户上下文信息。

接下来我们把用户信息以用户ID为key存入ConcurentHashMap中:

public class SharedMapWithUserContext implements Runnable {

    public static Map<Integer, Context> userContextPerUserId
= new ConcurrentHashMap<>();
private Integer userId;
private UserRepository userRepository = new UserRepository(); @Override
public void run() {
String userName = userRepository.getUserNameForUserId(userId);
userContextPerUserId.put(userId, new Context(userName));
} // standard constructor
}

我们来测试一下代码,给两个用户ID创建两个线程,在运行结束设置断言:userContextPerUserId的大小为2:

SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start(); assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);

4、存储用户数据在ThreadLocal中

我们重写一下我们的例子,这次把用户信息存在ThreadLocal中,每个线程都有自己的ThreadLocal实例。

我们在使用的时候要特别小心因为每个ThreadLocal实例都关联了一个特定的线程,在我们的例子中,我们给每个用户ID创建了一个专用的线程,并且这是我们自己创建出来的,我们可以完全控制它们。(为什么这么说后面会解释到)

run()方法拿到用户信息构造上下文对象并使用ThreadLocal的set()方法存储起来:

public class ThreadLocalWithUserContext implements Runnable {

    private static ThreadLocal<Context> userContext
= new ThreadLocal<>();
private Integer userId;
private UserRepository userRepository = new UserRepository(); @Override
public void run() {
String userName = userRepository.getUserNameForUserId(userId);
userContext.set(new Context(userName));
System.out.println("thread context for given userId: "
+ userId + " is: " + userContext.get());
} // standard constructor
}

我们开启两个线程测试一下:

ThreadLocalWithUserContext firstUser
= new ThreadLocalWithUserContext(1);
ThreadLocalWithUserContext secondUser
= new ThreadLocalWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

代码运行完能看到ThreadLocal在每个线程中都设置了值:

thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'}
thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}

5、小心把ThreadLocal和ExecutorService一起使用

  • 当前例子

    如果我们使用ExecutorService并往里面提交Runnable任务,使用ThreadLocal会出现不确定的结果。因为我们不能确定操作每个用户ID的Runnable任务每次都是被相同的线程操作,因此ThreadLocal会被不用的用户ID公用。

  • 不同的业务使用场景不同,也不能一棒子打死,比如下面这个例子

    最近在使用Spring的状态机的时候,处理一个动作发起的逻辑需要调用状态机实例的sendEvent方法发送消息,状态机返回当前事件是否处理成功,但不方便使用的是这个sendEvent返回的值是个boolean类型,里面如果有错误,我拿不到错误信息,抛异常也会被内部捕获到返回false:

boolean sendEvent(Message<E> event);

这个例子和上述不同的地方在于我使用状态机在每个Runnable任务执行中只需要拿到本次运行的信息,而不需要把当前信息和后面提交的任务共享使用。所以我调用get()拿到我在另一处逻辑里设置的错误信息时,我立即调用remove()表示本次ThreadLocal结束了。

状态机判断逻辑:

if (checkInvoice == null) {
@SuppressWarnings("unchecked")
ThreadLocal<String> requestError = (ThreadLocal<String>) messageHeaders.get(THREAD_LOCAL_NAME);
Objects.requireNonNull(requestError).set("发票不能为空");
return false;
}

状态机外部处理逻辑:

// 返回的结果是Guard的返回结果
boolean isHandleSuccess = stateMachine.sendEvent(message);
if (isHandleSuccess) {
S currentState = stateMachine.getState().getId();
if (previousState != currentState) {
stateMachinePersister.persist(stateMachine, uniqueKey);
} else {
log.info("状态没有变更,不需要持久化状态。previousState={},currentState={}",
previousState, currentState);
}
if (previousState.checkNextState(currentState)) {
return buildSuccessResult(object, event.desc() + SUCCESS_TEXT);
} else {
String error = requestError.get();
log.info("ThreadLocal value:{}", error);
requestError.remove();
return buildFailureResult(object, error);
}
}

人人都能学会系列之ThreadLocal的更多相关文章

  1. 人人都能学会的 Python 多线程指南~

    大家好鸭!有没有想我~(https://jq.qq.com/?_wv=1027&k=rX9CWKg4) 在 Python 中,多线程最常见的一个场景就是爬虫,例如这样一个需求,有多个结构一样的 ...

  2. 人人都是产品经理<1.0>

    用了大概2个月的时间,细细的读完了<人人都是产品经理>这本书,受益良多,期间也做了一些笔记,都在前面的博客————products系列中... 当然,更多的收获,还是沉滞在书中的注释,以及 ...

  3. 人人都是 DBA(XII)查询信息收集脚本汇编

    什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA& ...

  4. 人人都是 DBA(XV)锁信息收集脚本汇编

    什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA& ...

  5. 人人都是 DBA(XIV)存储过程信息收集脚本汇编

    什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA& ...

  6. 人人都是 DBA(XIII)索引信息收集脚本汇编

    什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA& ...

  7. 人人都是 DBA(XI)I/O 信息收集脚本汇编

    什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA& ...

  8. 人人都是 DBA(X)资源信息收集脚本汇编

    什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA& ...

  9. 人人都是 DBA(IX)服务器信息收集脚本汇编

    什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA& ...

随机推荐

  1. Bitmap缩放(一)

    使用矩阵进行压缩,通过缩放图片尺寸,来达到压缩图片的效果,和采样率的原理一样.先用位图的option将位图压缩一半,再用matrix缩放0.3f public class MainActivity e ...

  2. java并发与多线程面试题与问题集合

    http://www.importnew.com/12773.html     https://blog.csdn.net/u011163372/article/details/73995897    ...

  3. requests库响应消息体的四种格式

    1.r.text  文本响应内容,返回字符串类型,获取网页html时用: 2.r.content  字节响应内容,返回字节类型,下载图片或者文件时用: 3.r.json json解码响应内容,返回字典 ...

  4. 4G DTU模块的功能和作用是什么

    4G DTU模块我们可以简单将它理解为使用4G无线通信网络来进行远距离无线传送的终端设备.4G DTU模块基于4G方式进行远距离的数据传输,是专门用于将串口数据转换为IP数据或将IP数据转换为串口数据 ...

  5. Java学习的第四十五

    1.例5.7求三个字符串中的最大值 import java.util.Scanner; public class Cjava { public static void main(String[]arg ...

  6. nginx处理vue打包文件后的跨域问题

    起因 在vue文件打包后,项目脱离了vue配置的反向代理配置,还是会报跨域的错误,或者直接打不开本地文件, 但是此刻我们想打开打包后的文件,测试一下文件有没有错误,因为经常会存在开发阶段没有问题,打包 ...

  7. [Luogu P1122]最大子树和 (简单树形DP)

    题面 传送门:https://www.luogu.org/problemnew/show/P1122 Solution 这是一道简单的树形DP题. 首先,我们可以转换一下题面,可以发现,题目要求我们求 ...

  8. 动态规划之KMP字符匹配算法

    KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高,但是确实有点复杂. 很多读者抱怨 KMP 算法无法理解,这很正常,想到大学教材上关于 KMP 算法的讲解 ...

  9. vim实现CTRL+S为保存快捷键

    用vim正撸代码撸的飞起,突然Xshell就掉线了,真是太蛋疼了. 于是开始怀念起vs下撸代码时随时随地ctrl+s保存的快捷键,百度了一下,网上的vim实现ctrl+s保存的快捷键设置都有问题,自己 ...

  10. 3.6 栈 ADT - 3.7 队列 ADT

    3.6 栈 ADT 栈是限制插入和删除只能在一个位置上进行的表,叫做栈的顶部.对栈的基本操作有进栈和出栈,进栈在顶部插入元素,出栈删除最后插入的元素. 栈是一个表,因此任何实现表的方法都能实现栈.显然 ...