「学习笔记」深入理解 ThreadLocal
目录
一 引言
二 源码解析
三 案例
四 总结
一 引言
ThreadLocal 的官方 API 解释为:
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code 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).
这个类提供线程局部变量。这些变量与正常的变量不同,每个线程访问一个(通过它的 get 或 set 方法)都有它自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,希望将状态与线程关联(例如,用户 ID 或事务 ID)。
1、当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
2、使用ThreadLocal通常是定义为 private static ,更好是 private final static
3、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
4、ThreadLocal类封装了getMap()、Set()、Get()、Remove()4个核心方法
从表面上来看 ThreadLocal 内部是封闭了一个 Map 数组,来实现对象的线程封闭,map 的 key 就是当前的线程 id,value 就是我们要存储的对象。
实际上是 ThreadLocal 的静态内部类 ThreadLocalMap 为每个 Thread 都维护了一个数组 table,hreadLocal 确定了一个数组下标,而这个下标就是 value 存储的对应位置,继承自弱引用,用来保存 ThreadLocal 和 Value 之间的对应关系,之所以用弱引用,是为了解决线程与 ThreadLocal 之间的强绑定关系,会导致如果线程没有被回收,则 GC 便一直无法回收这部分内容。
二 源码剖析
2.1 ThreadLocal
//set方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//实际存储的数据结构类型
ThreadLocalMap map = getMap(t);
//判断map是否为空,如果有就set当前对象,没有创建一个ThreadLocalMap
//并且将其中的值放入创建对象中
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//get方法
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//实际存储的数据结构类型
ThreadLocalMap map = getMap(t);
if (map != null) {
//传入了当前线程的ID,到底层Map Entry里面去取
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);//调用ThreadLocalMap删除变量
}
//ThreadLocalMap中getEntry方法
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);
}
//getMap()方法
ThreadLocalMap getMap(Thread t) {
//Thread中维护了一个ThreadLocalMap
return t.threadLocals;
}
//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;
}
//createMap()方法
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
从上面源码中我们看到不管是 set() get() remove() 他们都是操作 ThreadLocalMap 这个静态内部类的,每一个新的线程 Thread 都会实例化一个 ThreadLocalMap 并赋值给成员变量 threadLocals,使用时若已经存在 threadLocals 则直接使用已经存在的对象
ThreadLocal.get()
获取当前线程对应的 ThreadLocalMap
如果当前 ThreadLocal 对象对应的 Entry 还存在,并且返回对应的值
如果获取到的 ThreadLocalMap 为 null,则证明还没有初始化,就调用 setInitialValue()方法
ThreadLocal.set()
获取当前线程,根据当前线程获取对应的 ThreadLocalMap
如果对应的 ThreadLocalMap 不为 null,则调用 set 方法保存对应关系
如果为 null,创建一个并保存 k-v 关系
ThreadLocal.remove()
获取当前线程,根据当前线程获取对应的 ThreadLocalMap
如果对应的 ThreadLocalMap 不为 null,则调用 ThreadLocalMap 中的 remove 方法,根据 key.threadLocalHashCode & (len-1)获取当前下标并移除
成功后调用 expungeStaleEntry 进行一次连续段清理
2.2 ThreadLocalMap
ThreadLocalMap 是 ThreadLocal 的一个内部类
static class ThreadLocalMap {
/**
* 自定义一个Entry类,并继承自弱引用
* 同时让ThreadLocal和储值形成key-value的关系
* 之所以用弱引用,是为了解决线程与ThreadLocal之间的强绑定关系
* 会导致如果线程没有被回收,则GC便一直无法回收这部分内容
*
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* Entry数组的初始化大小(初始化长度16,后续每次都是2倍扩容)
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 根据需要调整大小
* 长度必须是2的N次幂
*/
private Entry[] table;
/**
* The number of entries in the table.
* table中的个数
*/
private int size = 0;
/**
* The next size value at which to resize.
* 下一个要调整大小的大小值(扩容的阈值)
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
* 根据长度计算扩容阈值
* 保持一定的负债系数
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Increment i modulo len
* nextIndex:从字面意思我们可以看出来就是获取下一个索引
* 获取下一个索引,超出长度则返回
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
* 返回上一个索引,如果-1为负数,返回长度-1的索引
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* ThreadLocalMap构造方法
* ThreadLocalMaps是延迟构造的,因此只有在至少要放置一个节点时才创建一个
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//内部成员数组,INITIAL_CAPACITY值为16的常量
table = new Entry[INITIAL_CAPACITY];
//通过threadLocalHashCode(HashCode) & (长度-1)的位运算,确定键值对的位置
//位运算,结果与取模相同,计算出需要存放的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 创建一个新节点保存在table当中
table[i] = new Entry(firstKey, firstValue);
//设置table元素为1
size = 1;
//根据长度计算扩容阈值
setThreshold(INITIAL_CAPACITY);
}
/**
* 构造一个包含所有可继承ThreadLocals的新映射,只能createInheritedMap调用
* ThreadLocal本身是线程隔离的,一般来说是不会出现数据共享和传递的行为
*/
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++;
}
}
}
}
/**
* ThreadLocalMap中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
// 找不到的话接着从i位置开始向后遍历,基于线性探测法,是有可能在i之后的位置找到的
return getEntryAfterMiss(key, i, e);
}
/**
* ThreadLocalMap的set方法
*/
private void set(ThreadLocal<?> key, Object value) {
//新开一个引用指向table
Entry[] tab = table;
//获取table长度
int len = tab.length;
////获取索引值,threadLocalHashCode进行一个位运算(取模)得到索引i
int i = key.threadLocalHashCode & (len-1);
/**
* 遍历tab如果已经存在(key)则更新值(value)
* 如果该key已经被回收失效,则替换该失效的key
**/
//
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
//如果 k 为null,则替换当前失效的k所在Entry节点
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果上面没有遍历成功则创建新值
tab[i] = new Entry(key, value);
// table内元素size自增
int sz = ++size;
//满足条件数组扩容x2
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* remove方法
* 将ThreadLocal对象对应的Entry节点从table当中删除
*/
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();//将引用设置null,方便GC回收
expungeStaleEntry(i);//从i的位置开始连续段清理工作
return;
}
}
}
/**
* ThreadLocalMap中replaceStaleEntry方法
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
// 新建一个引用指向table
Entry[] tab = table;
//获取table的长度
int len = tab.length;
Entry e;
// 记录当前失效的节点下标
int slotToExpunge = staleSlot;
/**
* 通过prevIndex(staleSlot, len)可以看出,由staleSlot下标向前扫描
* 查找并记录最前位置value为null的下标
*/
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// nextIndex(staleSlot, len)可以看出,这个是向后扫描
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 获取Entry节点对应的ThreadLocal对象
ThreadLocal<?> k = e.get();
//如果和新的key相等的话,就直接赋值给value,替换i和staleSlot的下标
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果之前的元素存在,则开始调用cleanSomeSlots清理
if (slotToExpunge == staleSlot)
slotToExpunge = i;
/**
*在调用cleanSomeSlots() 清理之前,会调用
*expungeStaleEntry()从slotToExpunge到table下标所在为
*null的连续段进行一次清理,返回值就是table为null的下标
*然后以该下标 len进行一次启发式清理
* 最终里面的方法实际上还是调用了expungeStaleEntry
* 可以看出expungeStaleEntry方法是ThreadLocal核心的清理函数
*/
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果在table中没有找到这个key,则直接在当前位置new Entry(key, value)
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果有其他过时的节点正在运行,会将它们进行清除,slotToExpunge会被重新赋值
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* expungeStaleEntry() 启发式地清理被回收的Entry
* 有两个地方调用到这个方法
* 1、set方法,在判断是否需要resize之前,会清理并rehash一遍
* 2、替换失效的节点时候,也会进行一次清理
*/
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];
//判断如果Entry对象不为空
if (e != null && e.get() == null) {
n = len;
removed = true;
//调用该方法进行回收,
//对 i 开始到table所在下标为null的范围内进行一次清理和rehash
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
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;
}
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
/**
* 对table进行扩容,因为要保证table的长度是2的幂,所以扩容就扩大2倍
*/
private void resize() {
//获取旧table的长度
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
//创建一个长度为旧长度2倍的Entry数组
Entry[] newTab = new Entry[newLen];
//记录插入的有效Entry节点数
int count = 0;
/**
* 从下标0开始,逐个向后遍历插入到新的table当中
* 通过hashcode & len - 1计算下标,如果该位置已经有Entry数组,则通过线性探测向后探测插入
*/
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {//如遇到key已经为null,则value设置null,方便GC回收
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 重新设置扩容的阈值
setThreshold(newLen);
// 更新size
size = count;
// 指向新的Entry数组
table = newTab;
}
}
ThreadLocalMap.set()
key.threadLocalHashCode & (len-1),将 threadLocalHashCode 进行一个位运算(取模)得到索引 " i " ,也就是在 table 中的下标
for 循环遍历,如果 Entry 中的 key 和我们的需要操作的 ThreadLocal 的相等,这直接赋值替换
如果拿到的 key 为 null ,则调用 replaceStaleEntry()进行替换
如果上面的条件都没有成功满足,直接在计算的下标中创建新值
在进行一次清理之后,调用 rehash()下的 resize()进行扩容
ThreadLocalMap.expungeStaleEntry()
这是 ThreadLocal 中一个核心的清理方法
为什么需要清理?
在我们 Entry 中,如果有很多节点是已经过时或者回收了,但是在 table 数组中继续存在,会导致资源浪费
我们在清理节点的同时,也会将后面的 Entry 节点,重新排序,调整 Entry 大小,这样我们在取值(get())的时候,可以快速定位资源,加快我们的程序的获取效率
ThreadLocalMap.remove()
我们在使用 remove 节点的时候,会使用线性探测的方式,找到当前的 key
如果当前 key 一致,调用 clear()将引用指向 null
从"i"开始的位置进行一次连续段清理
三 案例
目录结构:
在这里插入图片描述
HttpFilter.java
package com.lyy.threadlocal.config;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Slf4j
public class HttpFilter implements Filter {
//初始化需要做的事情
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
//核心操作在这个里面
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
// request.getSession().getAttribute("user");
System.out.println("do filter:"+Thread.currentThread().getId()+":"+request.getServletPath());
RequestHolder.add(Thread.currentThread().getId());
//让这个请求完,,同时做下一步处理
filterChain.doFilter(servletRequest,servletResponse);
}
//不再使用的时候做的事情
@Override
public void destroy() {
}
}
HttpInterceptor.java
package com.lyy.threadlocal.config;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class HttpInterceptor extends HandlerInterceptorAdapter {
//接口处理之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle:");
return true;
}
//接口处理之后
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
RequestHolder.remove();
System.out.println("afterCompletion");
return;
}
}
RequestHolder.java
package com.lyy.threadlocal.config;
public class RequestHolder {
private final static ThreadLocal<Long> requestHolder = new ThreadLocal<>();//
//提供方法传递数据
public static void add(Long id){
requestHolder.set(id);
}
public static Long getId(){
//传入了当前线程的ID,到底层Map里面去取
return requestHolder.get();
}
//移除变量信息,否则会造成逸出,导致内容永远不会释放掉
public static void remove(){
requestHolder.remove();
}
}
ThreadLocalController.java
package com.lyy.threadlocal.controller;
import com.lyy.threadlocal.config.RequestHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/thredLocal")
public class ThreadLocalController {
@RequestMapping("test")
@ResponseBody
public Long test(){
return RequestHolder.getId();
}
}
ThreadlocalDemoApplication.java
package com.lyy.threadlocal;
import com.lyy.threadlocal.config.HttpFilter;
import com.lyy.threadlocal.config.HttpInterceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@SpringBootApplication
public class ThreadlocalDemoApplication extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(ThreadlocalDemoApplication.class, args);
}
@Bean
public FilterRegistrationBean httpFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new HttpFilter());
registrationBean.addUrlPatterns("/thredLocal/*");
return registrationBean;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HttpInterceptor()).addPathPatterns("/**");
}
}
输入:http://localhost:8080/thredLocal/test
后台打印:
do filter:35:/thredLocal/test preHandle:
afterCompletion
四 总结
1、ThreadLocal 是通过每个线程单独一份存储空间,每个 ThreadLocal 只能保存一个变量副本。
2、相比于 Synchronized,ThreadLocal 具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值,很好的实现了线程封闭。
3、每次使用完 ThreadLocal,都调用它的 remove()方法,清除数据,避免内存泄漏的风险
4、通过上面的源码分析,我们也可以看到大神在写代码的时候会考虑到整体实现的方方面面,一些逻辑上的处理是真严谨的,我们在看源代码的时候不能只是做了解,也要看到别人实现功能后面的目的。
推荐阅读
看完三件事
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
关注公众号 『 Java 斗帝 』,不定期分享原创知识。
同时可以期待后续文章 ing🚀
Java架构师迁哥
还未添加个人签名 2020.09.07 加入
还未添加个人简介
评论