工作中,相信很多朋友都听过或者用过 ThreadLocal
。这篇文章就来说说它是什么?具体怎么玩?还有分析 ThreadLocal
常用API对应的源码。
1 场景案例:演示身份认证到业务处理
在生产环境中,通常是多个请求并发请求到服务器,一般会经过“身份认证与鉴权”和“业务处理”两个步骤。
下面我们来模拟一下大概的实现逻辑,相信大多数朋友的项目中,代码逻辑类似下面的代码:
public class NoThreadLocalTest {
/**
* 用 map 模拟数据源
*/
private Map userMap;
/**
* 执行测试用例前,初始化数据源
*/
@Before
public void initUserList() {
userMap = new ConcurrentHashMap<>();
User u1 = new User(1, "陈一");
User u2 = new User(2, "钱二");
User u3 = new User(3, "张三");
User u4 = new User(4, "李四");
User u5 = new User(5, "王五");
User u6 = new User(6, "赵六");
userMap.put(u1.getId(), u1);
userMap.put(u2.getId(), u2);
userMap.put(u3.getId(), u3);
userMap.put(u4.getId(), u4);
userMap.put(u5.getId(), u5);
userMap.put(u6.getId(), u6);
}
/**
* 模拟从数据库中,根据 id 查询 user
*
* @param id
* @return
*/
private User getById(Long id) {
return this.userMap.get(id);
}
/**
* 模拟项目的 Filter 或者 Interceptor 层,身份认证和鉴权环节
*
* @param id
*/
private void doAuth(Long id) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "开始进行身份认证");
// 从数据库中,根据 id 查询 user
User user = this.getById(id);
if (user == null)
throw new NullPointerException("user is null");
// 省略其他校验逻辑
// 模拟身份认证处理的耗时,这里设置 50ms
TimeUnit.MILLISECONDS.sleep(50);
System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "身份认证通过");
}
/**
* 模拟项目的 service 层,进行业务处理
*
* @param id
*/
private void doService(Long id) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "开始进行业务处理");
// 再次从数据库中,根据 id 查询 user
User user = this.getById(id);
if (user == null)
throw new NullPointerException("user is null");
// 省略其他业务处理逻辑
// 模拟业务处理的耗时,这里设置 100ms
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "身份认证业务处理");
}
/**
* 测试不使用 ThreadLocal,当需要使用 user 时,每次都从数据库查询用户信息
*/
@Test
public void test() throws InterruptedException {
List threads = new ArrayList<>();
// 新建 6 条请求线程,模拟生产环境中并发请求场景
for (long i = 1; i <= this.userMap.size(); i++) {
long id = i;
Thread requestThread = new Thread(new Runnable() {
@Override
public void run() {
try {
// 模拟项目中,每个请求都要进行 身份认证和业务处理 两步逻辑
doAuth(id);
doService(id);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads.add(requestThread);
}
// 启动所有的请求线程
for (Thread thread : threads) {
thread.start();
}
TimeUnit.SECONDS.sleep(5);
}
}
运行结果,正常:
Thread-0-- 1 --开始进行身份认证
Thread-1-- 2 --开始进行身份认证
Thread-2-- 3 --开始进行身份认证
Thread-3-- 4 --开始进行身份认证
Thread-4-- 5 --开始进行身份认证
Thread-5-- 6 --开始进行身份认证
Thread-5-- 6 --身份认证通过
Thread-5-- 6 --开始进行业务处理
Thread-3-- 4 --身份认证通过
Thread-2-- 3 --身份认证通过
Thread-2-- 3 --开始进行业务处理
Thread-1-- 2 --身份认证通过
Thread-1-- 2 --开始进行业务处理
Thread-0-- 1 --身份认证通过
Thread-0-- 1 --开始进行业务处理
Thread-4-- 5 --身份认证通过
Thread-3-- 4 --开始进行业务处理
Thread-4-- 5 --开始进行业务处理
Thread-1-- 2 --身份认证业务处理
Thread-4-- 5 --身份认证业务处理
Thread-3-- 4 --身份认证业务处理
Thread-5-- 6 --身份认证业务处理
Thread-0-- 1 --身份认证业务处理
Thread-2-- 3 --身份认证业务处理
分析:
处理结果正常,但是代码中我们可以看到,在 doAuth(id)
和 doService(id)
这两个方法中,都执行了 this.getById(id);
根据 id 查询数据库。这是一个重复的操作,每次操作数据库,都会存在网络延迟,对数据库也会产生查询压力,同时服务器端要建立数据库连接,也需要消耗资源和性能。
那有没有什么办法可以避免多次 getById(id)
查库呢?
从业务逻辑上,当服务到接收到请求时,会分配一个线程(后面用 requestThread
表示)专门来处理该请求。身份认证环节 doAuth(id)
中通过 getById(id)
查询到用户信息 user
,当认证通过说明当前请求的是合法的用户,同时,我们也已经获取到了用户信息,请求线程 requestThread
中的后续操作如:doService(id)
,我们可以想办法直接使用身份认证环节查询到的 user
,而不用再次 getById(id)
查库。
这时,大家应该都会想到一种的数据结构,Map
或者存取操作线程安全的 ConcurrentHashMap
。
比如:Map userCacheMap = new ConcurrentHashMap<>()
;
但是,userCacheMap
是一个公共资源,多个线程都能够同时操作这个 ``ConcurrentHashMap,比如
requestThread2可以修改或者删除
requestThread1在
userCacheMap` 中存放的数据,这时很不安全的,也很难去维护。
这时,ThreadLocal
就闪亮登场了。
2 ThreadLocal
>ThreadLocal的用法类似 Map
(内部实现原理和 Map
不一样,下面会分析),而且,所有线程的操作都是线程隔离的,也就是说每个线程只能操作自己线程相关的资源,通过 get()
、 set()
和 remove()
等方法操作的都是当前线程对应的值,线程安全。
2.1 ThreadLocal 怎么用?API
方法 | 描述 |
---|---|
initialValue() | 若当前 Thread 没有在 ThreadLocal 中 set 过任何的值,则当该线程调用 ThreadLocal.get() 时,会调用 initialValue() 返回初始值,默认是 null 。可以重写该方法,设置你想返回的初始值 |
set(T value) | 为当前 Thread 在 ThreadLocal 中 set 新值 |
T get() | 获取当前 Thread 在 ThreadLocal 中的 value ,若当前 Thread 没有在 ThreadLocal 中 set 过任何的值,则当该线程调用 ThreadLocal.get() 时,会调用 initialValue() 返回初始值,默认是 null 。 |
void remove() | 删除当前 Thread 在 ThreadLocal 中对应的 value |
2.2 使用 ThreadLocal 设计 第一节中的案例
在代码中,设置一个公共的 ThreadLocal 变量
,用于保存各个请求线程中的资源,各个线程的操作的都是线程隔离的。
public class UseThreadLocalTest {
/**
* 用 map 模拟数据源
*/
private Map userMap;
/**
* threadlocal,用于保存各个请求线程中的资源
*/
private ThreadLocal userThreadLocal = new ThreadLocal<>();
/**
* 执行测试用例前,初始化数据源
*/
@Before
public void initUserList() {
userMap = new ConcurrentHashMap<>();
User u1 = new User(1, "陈一");
User u2 = new User(2, "钱二");
User u3 = new User(3, "张三");
User u4 = new User(4, "李四");
User u5 = new User(5, "王五");
User u6 = new User(6, "赵六");
userMap.put(u1.getId(), u1);
userMap.put(u2.getId(), u2);
userMap.put(u3.getId(), u3);
userMap.put(u4.getId(), u4);
userMap.put(u5.getId(), u5);
userMap.put(u6.getId(), u6);
}
/**
* 模拟从数据库中,根据 id 查询 user
*
* @param id
* @return
*/
private User getById(Long id) {
return this.userMap.get(id);
}
/**
* 模拟项目的 Filter 或者 Interceptor 层,身份认证和鉴权环节
*
* @param id
*/
private void doAuth(Long id) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "开始进行身份认证");
// 从数据库中,根据 id 查询 user
User user = this.getById(id);
if (user == null)
throw new NullPointerException("user is null");
// 省略其他校验逻辑
// 模拟身份认证处理的耗时,这里设置 50ms
TimeUnit.MILLISECONDS.sleep(50);
// 身份认证通过后,将 user 缓存到 ThreadLocal 中
this.userThreadLocal.set(user);
System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "身份认证通过");
}
/**
* 模拟项目的 service 层,进行业务处理
*
* @param id
*/
private void doService(Long id) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "开始进行业务处理");
// 因为在身份认证通过后,我们已经把 id 对应的 user 信息缓存到了 ThreadLocal 中,所以,这里我们只需要从 threadLocal get 出 id 对应的 user
User user = this.userThreadLocal.get();
if (user == null)
throw new NullPointerException("user is null");
// 省略其他业务处理逻辑
// 模拟业务处理的耗时,这里设置 100ms
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread().getName() + "-- " + id + " --" + "身份认证业务处理");
// 当前请求业务处理完成后,将 ThreadLocal 中缓存的当前线程的数据删除
this.userThreadLocal.remove();
}
/**
* 使用 ThreadLocal,身份认证通过时,用 ThreadLocal 缓存 user ,
* 再当前线程中,其他缓解需要用到 user 信息时,从 ThreadLocal 中直接 get(),
* 请求线程结束时,再删除ThreadLocal中当前线程的数据
*/
@Test
public void test() throws InterruptedException {
List threads = new ArrayList<>();
// 新建 6 条请求线程,模拟生产环境中并发请求场景
for (long i = 1; i <= this.userMap.size(); i++) {
long id = i;
Thread requestThread = new Thread(new Runnable() {
@Override
public void run() {
try {
// 模拟项目中,每个请求都要进行 身份认证和业务处理 两步逻辑
doAuth(id);
doService(id);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads.add(requestThread);
}
// 启动所有的请求线程
for (Thread thread : threads) {
thread.start();
}
TimeUnit.SECONDS.sleep(5);
}
}
运行依然线程是线程安全的,每个线程在 doService(id)
方法中都能通过 ThreadLocal
读取到当前线程在 doAuth(id)
方法设置的 user
,避免了多次 getById(id)
查询数据库。
2.3 ThreadLocal 原理详解
Thread
,ThreadLocal
以及 ThreadLocalMap
三者之间的关系:
- 每个Thread对象中都持有一个
ThreadLocalMap
类型的成员变量threadLocals
。 ThreadLocalMap
中的有一个Entry数组(Entry[]
),key
是ThreadLocal
实例,value
是线程在ThreadLocal.set(value)
中设置的value
。
原理图:
2.4 源码分析
Thread类中ThreadLocalMap
Thread
类中有一个 ThreadLocalMap
类型的成员变量 threadLocals
public class Thread implements Runnable {
...
// Thread 类中持有一个 ThreadLocalMap 成员变量
ThreadLocal.c threadLocals = null;
...
}
ThreadLocal的部分源码
public class ThreadLocal {
...
// 获取当前 `Thread` 在 `ThreadLocal` 中的 `value`
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;
}
}
// map为null时,新建map,通过initialValue()设置并返回初始值
return setInitialValue();
}
// 获取 Thread 中的 ThreadLocalMap 变量
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 设置初始值
private T setInitialValue() {
// 获取初始值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 设置当前线程在ThreadLocal的值为初始值
map.set(this, value);
else
// Thread 的 ThreadLocalMap 变量初始化,并设置初始值
createMap(t, value);
return value;
}
// 获取初始值的方法,可重写
protected T initialValue() {
return null;
}
// 当前 Thread 在 ThreadLocal 中 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);
}
// Thread 的 ThreadLocalMap 变量初始化,并设置初始值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 删除当前 `Thread` 在 `ThreadLocal` 中对应的 `value`
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
...
// 看看 ThreadLocal 中 ThreadLocalMap 的类结构
static class ThreadLocalMap {
// Entry 是一种 弱引用
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* Entry[] 的初始容量 16
*/
private static final int INITIAL_CAPACITY = 16;
/**
* Entry数组,每个Entry都保存了 key(ThreadLocal) 和 value
*/
private Entry[] table;
}
...
}
这篇文章,通过模拟实际项目中的一个场景,给大家演示了 ThreadLocal
的使用,它能保证各个线程对 ThreadLocal
的操作的都是线程隔离的,从而保证线程安全,安全地保存当前线程的数据。
同时,也列举和通过源码分析了 ThreadLocal
各个API的使用。
>代码:
>github.com/wengxingxia/002juc.git