前言

业务开发中经常使用 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. 081 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 01 初识面向对象 06 new关键字

    081 01 Android 零基础入门 02 Java面向对象 01 Java面向对象基础 01 初识面向对象 06 new关键字 本文知识点:new关键字 说明:因为时间紧张,本人写博客过程中只是 ...

  2. Java 异常 Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date'

    查询时发送给服务器的日期的字符串格式:yyyy-MM-dd HH:mm:ss 服务器接收到日期的字符串之后,向 MySQL 数据库发起查询时,因为没有指定日期时间格式,导致字符串数据不能正确地转换为日 ...

  3. P3660 [USACO17FEB]Why Did the Cow Cross the Road III G

    Link 题意: 给定长度为 \(2N\) 的序列,\(1~N\) 各处现过 \(2\) 次,i第一次出现位置记为\(ai\),第二次记为\(bi\),求满足\(ai<aj<bi<b ...

  4. CentOS 7安装docker和常用指令

    1.安装 yum -y install docker 2.启动 systemctl start docker // 启动 docker -v //查看版本号 systemctl stop docker ...

  5. 深入浅出具有划时代意义的G1垃圾回收器

    G1诞生的背景 Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式.HotSpot开发团队最初 ...

  6. 教你怎么在thinkphp 5.1下查看版本号

    在thinkphp 5.1下查看版本号,可直接命令行下面 php think version,就可以查看到tp具体的版本号了.

  7. java高级项目 jdbc与数据库连接数据库

    //图书管类 public class Book { private Integer id; private String b_name; private double b_price; privat ...

  8. antd pro 路由

    概要 antd pro 路由简介 路由, 菜单和面包屑 页面之间的路由 带参数的路由 总结 概要 路由配置是单页应用的核心之一, antd pro 将所有的路由配置集中在一个文件中, 可以更好的对应用 ...

  9. 写了多年代码,你会 StackOverflow 吗

    写了多年代码,你会 StackOverflow 吗 Intro 准备写一个傻逼代码的系列文章,怎么写 StackOverflow 的代码,怎么写死锁代码,怎么写一个把 CPU 跑满,怎么写一个 Out ...

  10. vue知识点13

    知识点归纳整理如下: 组件 component     1.页面中的一部分,可以复用, 本质上是一个拥有预定义选项的一个 Vue 实例         2.使用         1)定义        ...