前言

业务开发中经常使用 ThreadLocal 来存储用户信息等线程私有对象... ThreadLocal 内部构造是什么样子的?为什么可以线程私有?常说的内存泄露又是怎么回事?

公众号:liuzhihangs ,记录工作学习中的技术、开发及源码笔记;时不时分享一些生活中的见闻感悟。欢迎大佬来指导!

介绍

ThreadLocal 类提供了线程局部变量。和正常对象不同的是,每个线程都可以访问 get()、set() 方法,获取独属于自己的副本。 ThreadLocal 实例通常是类中的私有静态字段,并且其状态和线程关联。

每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例访问; 一个线程消失之后,所有的线程局部实例的副本都会被垃圾回收(除非存在对这些副本的其他引用)。

使用

有这么一种使用场景,收到 web 请求,先进行 token 验证,而这个 token,可以解析出用户 user 的信息。所以我这边一般是这样使用的:

  1. 自定义注解, @CheckToken , 标识该方法需要校验 token。
  2. Interceptor(拦截器)中检查,如果方法有 @CheckToken 注解则校验 token。
  3. 从Header中获取 Authorization ,请求第三方或者自己的逻辑校验 token ,并解析成 user。
  4. 将user放到ThreadLocal中。
  5. controller、service 在后续使用中, 如果需要 user 信息,可以直接从 ThreadLocal 中获取。
  6. 使用结束后进行remove。

代码如下:


public class LocalUserUtils { /**
* 用户信息保存至 ThreadLocal 中
*/
private static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>(); public static void set(User user) {
USER_THREAD_LOCAL.set(user);
} public static User get() {
return USER_THREAD_LOCAL.get();
} public static void remove() {
USER_THREAD_LOCAL.remove();
} } /**
* 1. 加上注解 CheckToken
* 只有方法, 类忽略
*/
@CheckToken
@PostMapping("/doXxx")
public Result<Resp> doXxx(@RequestBody Req req) { Resp resp = xxxService.doXxx(req); return result.success(resp);
} /**
* 2. 3. 4.
*/
@Component
public class TokenInterceptor implements HandlerInterceptor { @Override
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
throws Exception {
LocalUserUtils.remove();
} @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 请求方法是否存在注解
boolean assignableFrom = handler.getClass().isAssignableFrom(HandlerMethod.class); if (!assignableFrom) {
return true;
} CheckToken checkToken = null;
if (handler instanceof HandlerMethod) {
checkToken = ((HandlerMethod) handler).getMethodAnnotation(CheckToken.class);
} // 没有加注解 直接放过
if (checkToken == null) {
return true;
} // 从Header中获取Authorization
String authorization = request.getHeader("Authorization");
log.info("header authorization : {}", authorization);
if (StringUtils.isBlank(authorization)) {
log.error("从Header中获取Authorization失败");
throw CustomExceptionEnum.NOT_HAVE_TOKEN.throwCustomException();
} User user = xxxUserService.checkAuthorization(authorization);
// 放到
LocalUserUtils.set(user); return true;
}
} /**
* 5. 使用
* 只有方法, 类忽略
*/
@Override
public Resp doXxx(Req req) { User user = LocalUserUtils.get(); // do something ... return resp;
}

抛出问题

  1. 为什么可以线程私有?
  2. 为什么建议声明为静态?
  3. 为什么强制使用后必须remove?

图 | 阿里巴巴 - Java开发手册(截图)

图 | 阿里巴巴 - Java开发手册(截图)

源码分析

Thread


public class Thread implements Runnable {
// 省略 ... ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 省略 ...
}

可以看出 Thread 对象中声明了 ThreadLocal.ThreadLocalMap 对象,每个线程都有自己的工作内存,每个线程都有自己的 ThreadLocal. ThreadLocalMap 对象,所以在线程之间是互相隔离的。

ThreadLocal

ThreadLocal则是一个泛型类,同时提供 set()get()remove()静态方法。

public class ThreadLocal<T> {

    // 线程本地hashCode
private final int threadLocalHashCode = nextHashCode(); // 获取此线程局部变量的当前线程副本中的值
public T get() {...}
// 设置当前线程的此线程局部变量的复制到指定的值
public void set(T value) {...}
// 删除当前线程的此线程局部变量的值
public void remove() {...}
// ThreadLocalMap只是用来维持线程本地值的定制Map
static class ThreadLocalMap {...}
}
set(T value)方法

public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 threadLocals 属性
ThreadLocalMap map = getMap(t);
if (map != null)
// 存在则赋值
map.set(this, value);
else
// 不存在则直接创建
createMap(t, value);
}
// 根据线程获取当前线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 创建ThreadLocalMap 并赋值给当前线程的threadLocals字段
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

1.Thread.currentThread() 先获取到当前线程。

2. 获取当前线程的 threadLocals 属性,即 ThreadLocalMap

3. 判断 Map 是否存在,存在则赋值,不存在则创建对象。

get()方法

public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 threadLocals 属性
ThreadLocalMap map = getMap(t);
// map不为空
if (map != null) {
// 根据当前ThreadLocal获取的ThreadLocalMap的Entry节点
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 获取节点的value 并返回
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 设置初始值并返回 (null)
return setInitialValue();
}

1.Thread.currentThread() 先获取到当前线程。

2. 获取当前线程的 threadLocals 属性,即 ThreadLocalMap

3. 判断 Map 不为空,根据当前 ThreadLocal 对象获取 ThreadLocalMap.Entry 节点, 从节点中获取 value。

4.ThreadLocalMap 为空或者 ThreadLocalMap.Entry 为空,则初始化 ThreadLocalMap 并返回。

remove()方法
public void remove() {
// 获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
// 不为空, 从ThreadLocalMap中移除该属性
if (m != null)
m.remove(this);
}

阅读 set()get()remove() 的源码之后发现后面其实是操作的 ThreadLocalMap, 主要还是操作的 ThreadLocalMapset()getEntry()remove() 以及构造函数。下面看是看 ThreadLocalMap 的源码。

ThreadLocalMap

static class ThreadLocalMap {

    /**
* Entry节点继承WeakReference是弱引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 与此ThreadLocal关联的值。 */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始容量-必须是2的幂
private static final int INITIAL_CAPACITY = 16; // 表,根据需要调整大小. table.length必须始终为2的幂.
private ThreadLocal.ThreadLocalMap.Entry[] table; // 表中的条目数。
private int size = 0; // 扩容阈值
private int threshold; // Default to 0
// 设置阀值为长度的 2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
// 构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {...} // 根据ThreadLocal获取节点Entry
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {...} // set ThreadLocalMap的k-v
private void set(ThreadLocal<?> key, Object value) {...} // 移除当前值
private void remove(ThreadLocal<?> key) {...}
}
  1. Entry 继承了 WeakReference<ThreadLocal<?> 也就意味着, Entry 节点的 key 是弱引用
  2. Entry 对象的key弱引用,指向的是 ThreadLocal 对象。
  3. 线程对象执行完毕,线程对象内实例属性会被回收,此时线程内 ThreadLocal 对象的引用被置为 null ,即 Entry 的 keynull, key 会被垃圾回收。
  4. ThreadLocal 对象通常为私有静态变量, 生命周期不会至少不会随着线程技术而结束。
  5. ThreadLocal 对象存在,并且 Entry的 key == null && value != null ,这时就会造成内存泄漏。
  • 小补充
  1. 强引用、软引用、弱引用、虚引用
强引用(StrongReference):最常见,直接 new Object(); 创建的即为强引用。当内存空间不足,Java虚拟机宁愿抛出 OOM,也不愿意随意回收具有强引用的对象来解决内存不足问题。
软引用(SoftReference):内存足够,垃圾回收器不会回收软引用对象;内存不足时,垃圾回收器会回收。
弱引用(WeakReference):垃圾回收器线程,发现就会回收。
虚引用(PhantomReference):任何时候都有可能被垃圾回收,必须引用队列联合使用。
  1. 内存泄露:
内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
—— 维基百科

构造函数及hash计算
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化Entry数组, 长度为16
table = new Entry[INITIAL_CAPACITY];
// 获取key的hashCode,并计算出在数组中的索引,
// 长度是 2的幂的情况下,取模 a % b == a & (b - 1)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
// 设置数组元素数
size = 1;
// 设置扩容阈值
setThreshold(INITIAL_CAPACITY);
}

threadLocalHashCode 是 ThreadLocal 的静态属性,通过 nextHashCode 方法获取。

private final int threadLocalHashCode = nextHashCode();

// 被赋予了接下来的哈希码。 原子更新。 从零开始。
private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
// 返回下一个hash码,通过步长 0x61c88647 累加生成,这块注释说明是最佳哈希值
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
  1. 初始化数组,长度16。
  2. 计算 key 的 hashCode,对2的幂取模。
  3. 设置元素,元素数及扩容阈值。

hashCode 通过步长 0x61c88647 累加生成, 并且使用了 AtomicInteger,保证原子性。

set()方法

private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table;
int len = tab.length;
// hashcode取模求数组索引
int i = key.threadLocalHashCode & (len-1); // 获取数组中对应的位置, 重点关注 e = tab[i = nextIndex(i, len)]
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 获取key
ThreadLocal<?> k = e.get();
// key 存在则覆盖
if (k == key) {
e.value = value;
return;
}
// key 不存在则赋值
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 此时 e == null 直接执创建节点
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlots 循环数组 查找全部key==null的Entry
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
  1. 获取循环 Entry 数组,获取 tab[i] 处的 e, e != null 继续循环

    1. 此时发现 e 的 key 不存在,并且不是 null (hash冲突了。)
    2. 那就通过 e = tab[i = nextIndex(i, len)]) 继续获取下一个 i,并获取新的 tab[i] 处的 e。
    3. 赋值替换值结束结束并返回。
  2. e == null 结束循环。
// 下一个index,如果 i + 1 < len 直接返回下一个位置
// 如果 i + 1 >= len 则返回 0, 从头开始。
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);
}
  1. 这块利用环形设计,如果长度到达数组长度,则从开头开始继续查找。
  2. int i = key.threadLocalHashCode & (len-1); 求出索引,并不是从0开始的。

/**
* staleSlot 为当前索引位置, 并且当前索引位置的 k == null
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e; // 需要清除的 entry 的索引
int slotToExpunge = staleSlot; // 循环获取到上一个 key==null 的节点及其索引,有可能还是自己
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i; // 继续上一层的循环,查找下一个 k == key 的节点索引
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get(); if (k == key) {
// key 相等 则直接赋值
e.value = value;
// 并且将 此处的 entry替换为 tab[staleSlot]
tab[i] = tab[staleSlot];
tab[staleSlot] = e; // 如果发现要清除的 entry和传入的在一个位置上, 则直接赋值
if (slotToExpunge == staleSlot)
slotToExpunge = i; // 清除掉过期的 expungeStaleEntry(slotToExpunge) 会清除 entry的value,将其设置为null并将其设置为null, 并返回下一个需要清除的entry的索引位置
// cleanSomeSlots 循环数组 查找全部key==null的Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
} // 如果向后扫描没有找到,并且已经到第初始传入的索引位置处了
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
} // 没找到, 直接将旧值 Entry 设置为 null 并指向新创建的Entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value); // 结束之后发现要清楚的 key的索引 不等于当前传入的索引, 说明还有其他需要清除。
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
  1. 这里存在三个属性 key, value,以及 staleSlot, staleSlot节点的 Entry != null 但是 k == null。
  2. 向前扫描获取到上一个 Entry != null 但是 k == null 的节点及其索引, 赋值给 slotToExpunge, 没有扫描到的话 slotToExpunge 还是等于 staleSlot。
  3. 向后扫描 Entry != null 的节点,因为在 set 方法中, 后面还有一段数组没有遍历。
    1. 发现 key 相等的Entry节点了, 直接赋值,然后清除其他 Entry != null 但是 k == null 的节点, 并返回。
    2. 没有找到key相等的节点,但是找到了下一个 Entry != null 但是 k == null, 且此时 slotToExpunge 未发生变化,还是指向 staleSlot, 则 i 赋值给 slotToExpunge。
  4. 向后扫描没有扫描到,则直接对当前节点(索引值为staleSlot)的节点的value设置为null,并指向新value。
  5. 结束之后发现 slotToExpunge 被改变了, 说明还有其他的要清除。
getEntry()方法

private Entry getEntry(ThreadLocal<?> key) {
// hashcode取模求数组索引
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)
// key 已经 == null 了 清除一下 value
expungeStaleEntry(i);
else
// 继续获取下一个
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
  1. hashcode 取模求数组索引。
  2. 索引处获取到 Entry 则直接返回。
  3. 获取不到或者获取到的 Entry key 不相等时,有可能是因为 hash 冲突,被放到别的地方, 调用 getEntryAfterMiss 方法。
  4. getEntryAfterMiss 方法中。
    1. e == null 返回null。
    2. e != null 判断key, key相等返回 Entry, key == null, 那就需要清除这个节点,然后继续按照 nextIndex(i, len) 方法找下一个节点。

remove()方法


private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// hashcode 取模求数组索引
int i = key.threadLocalHashCode & (len-1);
// 清除当前节点的value
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 清楚对象引用
e.clear();
// value 指向 null
expungeStaleEntry(i);
return;
}
}
}
public void clear() {
this.referent = null;
}
  1. hashcode 取模求数组索引。
  2. 循环查找数组,将当前 key 的 Entry 的引用,将 value 设置为 null, 后面会被垃圾回收掉。

总结

为什么可以线程私有?

ThreadLocal 的 get()、set()、remove()方法中都有 Thread t = Thread.currentThread(); 操作的其实是本线程,获取本线程的ThreadLocalMap。

每个线程都有自己的 ThreadLocal,并且是将 value 存放在一个以 ThreadLocal 为 key 的 ThreadLocalMap 中的。所以线程间隔离。

为什么建议声明为静态?

Java开发手册已经给出说明,还有就是,如果 ThreadLocal 设置为非静态,那就是某个线程的实例类,这样的话就会失去了线程共享的本质属性。

为什么强制必须时候后remove()?

这块可以和内存泄露一块说明, 通过上面的 ThreadLocalMap 处关于弱引用的讲解已经说明会产生内存泄露。至于如何解决也给出了答案:

1.set() 时清除 Entry != null && key == null 的节点, 将其 value 设置为 null。

2.getEntry() 时清除当前 key 到 nextIndex(i, len)==null 之间的 Entry != null && key == null 的节点, 将其 value 设置为 null。

3.remove() 时清除指定key的 Entry != null && key == null 的节点, 将其 value 设置为 null。

之所以使用remove(),还是为了解决内存泄露的问题。

Last

  1. 使用时注意声明为 private static final
  2. 使用后要 remove()

请介绍下你了解的ThreadLocal,它的底层原理!的更多相关文章

  1. 请介绍下 adb、ddms、aapt 的作用

    adb 是 Android Debug Bridge ,Android 调试桥的意思 ddms 是 Dalvik Debug Monitor Service,dalvik 调试监视服务. aapt 即 ...

  2. java面试一日一题:请讲下对mysql的理解

    问题:请讲下对mysql的理解 分析:该问题主要考察对mysql的理解,基本概念及sql的执行流程 回答要点: 主要从以下几点去考虑, 1.mysql的整体架构? 2.mysql中每一个组件的作用? ...

  3. 下面就介绍下Android NDK的入门学习过程(转)

    为何要用到NDK? 概括来说主要分为以下几种情况: 1. 代码的保护,由于apk的java层代码很容易被反编译,而C/C++库反汇难度较大. 2. 在NDK中调用第三方C/C++库,因为大部分的开源库 ...

  4. 百亿级别数据量,又需要秒级响应的案例,需要什么系统支持呢?下面介绍下大数据实时分析工具Yonghong Z-Suite

    Yonghong Z-Suite 除了提供优秀的前端BI工具之外,Yonghong Z-Suite让用户可以选购分布式数据集市来支持实时大数据分析. 对于这种百亿级的大数据案例,Yonghong Z- ...

  5. 如何使用开源库,吐在VS2013发布之前,顺便介绍下V2013的新特性"Bootstrap"

    如何使用开源库,吐在VS2013发布之前,顺便介绍下VS2013的新特性"Bootstrap" 刚看到Visual Studio 2013 Preview - ASP.NET, M ...

  6. 我也介绍下sizeof与strlen的区别

    本节我也介绍下sizeof与strlen的区别,很简单,就几条: 1. sizeof是C++中的一个关键字,而strlen是C语言中的一个函数:2. sizeof求的是系统分配的内存总量,而strle ...

  7. 7,请描述下cookies,sessionStorage和localStorage的区别

    7,请描述下cookies,sessionStorage和localStorage的区别 首先,cookie是网站为了标识用户身份而储存在用户本地终端(client side,百科: 本地终端指与计算 ...

  8. 介绍下Shell中的${}、##和%%使用范例,本文给出了不同情况下得到的结果。

    介绍下Shell中的${}.##和%%使用范例,本文给出了不同情况下得到的结果.假设定义了一个变量为:代码如下:file=/dir1/dir2/dir3/my.file.txt可以用${ }分别替换得 ...

  9. 介绍下Java内存区域(运行时数据区)

    介绍下Java内存区域(运行时数据区) Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域.JDK 1.8 和之前的版本略有不同. 下图是 JDK 1.8 对JV ...

随机推荐

  1. Mice and Rice(queue的用法)

    Mice and Rice(queue的用法) Mice and Rice is the name of a programming contest in which each programmer ...

  2. matlab中nargin函数输入参数数目

    来源:https://ww2.mathworks.cn/help/matlab/ref/nargin.html?searchHighlight=nargin&s_tid=doc_srchtit ...

  3. 第一次使用HSDB

    今天看了几篇大佬关于HSDB使用的文章,自己也依样画葫芦的用来一下,强大的一匹!!! HSDB(Hotspot Debugger),JDK自带的工具,用于查看JVM运行时的状态. HSDB位于C:\P ...

  4. CSGO 服务端扩展插件开发记录之"DropClientReason"(1)

    最近开始接触到了CSGO这款游戏,还是老套路,就是想千方百计的从里面增添新的游戏功能,当然刚开始想做到游刃有余是有点困难, 跟之前做CS1.6的第三方开发一样,都得自己慢慢的摸索过来,纵然CSGO所使 ...

  5. pytest文档58-随机执行测试用例(pytest-random-order)

    前言 通常我们认为每个测试用例都是相互独立的,因此需要保证测试结果不依赖于测试顺序,以不同的顺序运行测试用例,可以得到相同的结果. pytest默认运行用例的顺序是按模块和用例命名的 ASCII 编码 ...

  6. SQL Server Management Studio (SSMS)单独安装,仅安装连接工具

    简单来说,SSMS是用于远程连接数据库与执行管理任务的一个工具.当安装SQL SERVER时,会默认安装.但也可以单独安装在不是数据库服务器的主机上. SQL Server Management St ...

  7. msyql查看连接数

    连接数 SHOW FULL PROCESSLIST 1.  查看允许的最大并发连接数 SHOW VARIABLES LIKE 'max_connections'; 2.  修改最大连接数 方法1:临时 ...

  8. linux wget指定下载目录和重命名

    当我们在使用wget命令下载文件时,通常会需要将文件下载到指定的目录,这时就可以使用 -P 参数来指定目录,如果指定的目录不存在,则会自动创建. 示例: p.p1 { margin: 0; font: ...

  9. ansible通过yum/dnf模块给受控机安装软件(ansible2.9.5)

    一,使用yum/dnf模块要注意的地方: 使用dnf软件安装/卸载时,需要有root权限, 所以要使用become参数 说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnb ...

  10. 技术债! 怎样简洁高效的实现多个 Enum 自由转换

    一:背景 1. 讲故事 前段时间和同事负责一个项目的两个业务模块,可能大家缺少沟通,导致本该定义一个 Enum 的地方结果我俩各自定义了一个,导致后面这两个 Enum 进行对接就烦了,为了方便理解,也 ...