一、引入
整篇文章,从以下几个方面来进行梳理
1、使用场景,详细介绍在工作中的使用场景
2、梳理其整体结构,和ThreadLocal相关联的类之间的关系
3、源码解析
- 构造器
- set方法
- get方法
- remove方法
- 如何解决hash冲突
5、总结
二、使用场景
2.1、使用
这里给一个简单的demo,来介绍以下ThreadLocal是如何使用的
csharp
public class Code2 {
public static void main(String[] args) {
//主线程里的值
local.set("hello");
new Thread(() -> {
local.set("Thread one");
System.out.println(Thread.currentThread().getName() + " : " + local.get());//Thread-0 : Thread one
}).start();
new Thread(() -> {
local.set("Thread two");
System.out.println(Thread.currentThread().getName() + " : " + local.get());//Thread-1 : Thread two
}).start();
System.out.println(Thread.currentThread().getName() + " : " + local.get());//main : hello
local.remove();
System.out.println(Thread.currentThread().getName() + " : " + local.get());//main : null
//用线程池模拟五个线程从ThreadLocal中取值,结果都为Null
getValue();//结果都为null
}
public static final ThreadLocal<String> local = new ThreadLocal<>();
public static final ExecutorService EXECUTORS = Executors.newFixedThreadPool(5);
public static void getValue(){
for (int i = 0; i < 5; i++) {
EXECUTORS.submit(() -> System.out.println(local.get()));
}
EXECUTORS.shutdown();
}
}
根据结果输出可以看到,使用ThreadLocal做到了线程之间的隔离
2.2、场景一:每个线程需要独享的对象
单独为每一个线程创建一个副本,这样每个线程都可以单独使用自己的副本,做到线程之间的隔离,确保线程安全
在实际工作中,经常用来保存线程不安全的工具类,比如典型的SimpleDateFormat
1、线程安全问题演示:
java
public class Code {
public static final ExecutorService executor = Executors.newFixedThreadPool(16);
//共用全局对象
public static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
//模拟1000个线程共享一个SimpleDateFormat对象
for (int i = 0; i < 1000; i++) {
int f = i;
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println(new Code().getDate(f));
}
});
}
executor.shutdown();
}
public String getDate(int timeStamp){
Date date = new Date(timeStamp * 1000);
return format.format(date);
}
}
查看结果很明显出现了重复数据

所有线程都共享一个SimpleDateFormat对象,很容易出现线程安全问题。
2、解决
方案一:每次使用都创建一个SimpleDateFormat对象
java
public class Code {
public static final ExecutorService executor = Executors.newFixedThreadPool(16);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int f = i;
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println(new Code().getDate(f));
}
});
}
executor.shutdown();
}
public String getDate(int timeStamp){
Date date = new Date(timeStamp * 1000);
//给每一个线程创建一个SimpleDateFormat对象不会有线程安全问题
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(date);
}
}
这种方案是让每一个线程都会创建一个SimpleDateFormat对象,这么多对象的创建是有开销的,并且对象的销毁也是有开销的,这么多重复对象存在于内存中,也是对内存的浪费,如果使用不当,容易造成GC频繁,不推荐使用,建议使用方案二。
方案二:使用ThreadLocal,让每个线程独享一个SimpleDateFormat对象
csharp
public class SimpleDateFormatUtils {
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static SimpleDateFormat getFormat() {
SimpleDateFormat simpleDateFormat = THREAD_LOCAL.get();
if(simpleDateFormat == null){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
return simpleDateFormat;
}
}
2.3、场景二:每个线程内需要保存的信息
在生产环境中,本人使用到的ThreadLocal的场景
- 登录成功后,存放用户的完整信息
- 灰度发布时,存放服务的版本信息
每个线程获取到的登录信息都是不一样的,用户登录,在拦截器里进行校验通过之后,直接将后续流程需要用到的信息存放到ThreadLocal中,后续方法就能直接从ThreadLocal中直接获取,避免了传参
- 登录成功后,存放用户的完整信息【权限信息+用户信息】

无需任何措施就能保证线程安全,每个线程都有自己独立的信息,可以做到线程隔离
- 做灰度发布时,根据版本号来走到对应的版本信息

三、整体结构
Thread、ThreadLocal、ThreadLocalMap三者的整体关系

演化一下:

每一个Thread线程都会对应一个ThreadLocalMap,底层是Entry类型的数组,Entry对象里有两个属性,分别对应key和value,key是当前ThreadLocal对象,并且是个弱引用,value是Object类型的强引用。
key是弱引用代表这只有有 GC 就会被回收
key被回收了,但是value还可能会存在,容易造成内存泄露
这就是为什么,必须使用完ThreadLocal,就一定要进行remove
四、源码分析
4.1、构造器
默认一个空构造器,初始化了一个ThreadLocal对象
csharp
// 默认的空构造器
public ThreadLocal() {
}
4.2、Set方法
scss
public void set(T value) {
// 从线程里取出当前线程
Thread t = Thread.currentThread();
// 从线程里取出ThreadLocalMap,头一次取出来的基本都是空
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
// 每一个线程都有一个属性参数
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
第一次线程进来时,ThreadLocalMap为空

执行完Set方法后,会给对应的线程的threadLocal属性里面进行初始化

ini
private void set(ThreadLocal<?> key, Object value) {
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)]) {
// 找到了相同的key 替换value
if (e.refersTo(key)) {
e.value = value;
return;
}
// 清理过期条目
if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}
// 新增key,value
tab[i] = new Entry(key, value);
int sz = ++size;
// 扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 下一个索引位置 +1 , 到达末尾回到 0
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
4.3、get方法
kotlin
public T get() {
// 传入当前线程的值
return get(Thread.currentThread());
}
private T get(Thread t) {
// 拿到当前线程的Map
ThreadLocalMap map = getMap(t);
if (map != null) {
// map里面封装值用的 Entry对象,key就是当前的ThreadLocal
// 从这里就可以判断出,一个 ThreadLocalMap里只维护一个entry -> 言外之意就是只能有一个key-val对
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 取出当前线程里的值,返回
T result = (T) e.value;
return result;
}
}
// 如果map是空的,初始化map
return setInitialValue(t);
}
// 拿到当前线程ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/*
* 每个 Thread都会维护一个 ThreadLocalMap
*/
ThreadLocal.ThreadLocalMap threadLocals;
从每个线程的 ThreadLocalMap里取出Entry对象,从 Entry对象里取出对应的值
4.4、remove方法
ini
public void remove() {
// 拿到当前线程
remove(Thread.currentThread());
}
private void remove(Thread t) {
// 从线程里取出 ThreadLocalMap
ThreadLocalMap m = getMap(t);
if (m != null) {
// key就是当前 ThreadLocal,根据key从map里移除掉val
m.remove(this);
}
}
private void remove(ThreadLocal<?> key) {
// 拿到Entry数组对象
Entry[] tab = table;
int len = tab.length;
// 根据key计算出具体在数组里的位置
int i = key.threadLocalHashCode & (len-1);
// 这里为什么要进行遍历?为了解决hash冲突,如果计算出来的i 出现 hash冲突,不是对应的key
// 从index = i 开始,依次往后查找,直到找到对应的key
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.refersTo(key)) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
五、总结
ThreadLocal提供了线程局部变量,使得每个线程都有自己独立的副本,解决了多线程环境下共享变量的安全问题。
- Thread内部维护一个 ThreadLocalMap来存储ThreadLocal变量
- 每个线程都有自己独立的 ThreadLocalMap
- key是 ThreadLocal实例,val是存储的值
ThreadLocal解决hash冲突的方式:
- 利用数组,冲突比对key,一样就替换,不一样继续下一个,直到线性探测完所有元素