本文参考并摘抄自java基础教程由浅入深全面解析threadlocal
总结:
1. 线程并发:在多线程并发的场景下
2. 传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
3. 线程隔离:每一个线程的变量都是独立的,不会相互影响
package com.fxsh.ThreadLocal;
import lombok.extern.slf4j.Slf4j;
/*
* 需求:线程隔离
* 在多线程并发的场景下,每个线程中的变量都是相互独立的
* 线程A:设置(变量1) 获取(变量1)
* 线程B: 设置(变量2) 获取(变量2)
* ThreadLocal:
* 1. set():将变量绑定到当前线程
* 2. get():获取当前线程绑定的变量
* */
@Slf4j(topic = "tl.d1")
public class MyDemo01 {
private String content;
ThreadLocal<String> threadLocal = new ThreadLocal<>();
public String getContent() {
//return content;
return threadLocal.get();
}
public void setContent(String content) {
// this.content = content;
//变量content绑定到当前变量
threadLocal.set(content);
}
public static void main(String[] args){
MyDemo01 threadLocalTest = new MyDemo01();
for (int i = 0; i < 5; i++) {
new Thread(()->{
/*
* 每个线程绑定一个变量,等一会儿获取这个变量
* */
threadLocalTest.setContent(Thread.currentThread().getName()+"的数据");
log.debug("--------------");
log.debug(Thread.currentThread().getName()+"-----" + threadLocalTest.getContent());
},"thread"+i).start();
}
}
}
ThreadLocal
类和synchronized
@Slf4j(topic = "tl.d2")
public class MyDemo02 {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args){
MyDemo01 threadLocalTest = new MyDemo01();
for (int i = 0; i < 5; i++) {
new Thread(()->{
/*
* 每个线程绑定一个变量,等一会儿获取这个变量
* */
synchronized (MyDemo02.class){
threadLocalTest.setContent(Thread.currentThread().getName()+"的数据");
log.debug("--------------");
log.debug(Thread.currentThread().getName()+"-----" + threadLocalTest.getContent());
}
},"thread"+i).start();
}
}
}
能够解决问题,但是加锁,导致并发性能降低。
ThreadLocal
类和synchronized
的区别synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用以时间换空间 的方式,只提供了一份变量,让不同的线程排队访问 | 采用以空间换时间的方式,为每个线程都提供一份变量的副本,从而实现同时访问而互不干扰 |
多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
ThreadLocal
的内部结构每个ThreadLocal
都创建一个Map
,然后用线程作为Map
的key
,要存储的局部变量作为Map
的value
,这样就能达到各个线程的局部变量的隔离效果。
每个Thread
维护一个ThreadLocalMap
,这个Map的key
是ThreadLocal
实例本身,value
才是真正要存储的值Object
。
具体的过程是这样的:
1) 每个Thread线程内部都有一个Map(ThreadLocalMap)
2) Map里面存储ThreadLocal
对象(key)和线程的变量副本(value)
3) Thread内部的Map是有ThreadLocal
维护的,由ThreadLocal
负责向Map中获取和设置线程的变量值
4) 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本隔离,互不干扰。
除了构造方法,ThreadLocal
对外暴露了一下4个方法:
方法声明 | 描述 |
---|---|
protected T initialValue() | 返回当前线程局部变量的初始值 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取此线程对象中维护的ThreadLocalMap
ThreadLocal.ThreadLocalMap map = this.getMap(t);
//判断Map是否存在
if (map != null) {
//Map存在,则设置此实体entry
map.set(this, value);
} else {
//1)当前线程Thread不存在ThreadLocalMap对象
//2) 调用createMap进行ThreadLocalMap对象的初始化
//3) 并将当前线程t和value作为第一个entry存放至ThreadLocalMap中
this.createMap(t, value);
}
}
ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
执行流程:
1)首先获取当前线程,并根据当前线程获取ThreadLocalMap
2)如果map不为空,则将参数设置到Map中(当前ThreadLocal
为key)
3)如果Map为空,那么给该线程创建Map,并设置参数
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取当前线程维护的ThreadLocalMap对象
ThreadLocal.ThreadLocalMap map = this.getMap(t);
//如果ThreadLocalMap对象不为空
if (map != null) {
//获取以当前ThreadLocal为Key的实体e
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
//如果e不为空
if (e != null) {
//获取存储实体e对应的value值
//即为我们想要的当前线程对应此ThreadLocal的值
T result = e.value;
return result;
}
}
/*
初始化:有两种情况会执行当前代码
1:当前线程的ThreadLocalMap对象不存在
2:当前线程的ThreadLocalMap对象存在,但是没有与当前ThreadLocal对象管理的Entry
*/
return this.setInitialValue();
}
private T setInitialValue() {
//initialValue()方法可以被子类重写,如果不重写,默认返回null
T value = this.initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程维护的ThreadLocalMap对象
ThreadLocal.ThreadLocalMap map = this.getMap(t);
//对map判空
if (map != null) {
//存在则设置此实体entry
map.set(this, value);
} else {
//1)当前线程Thread不存在ThreadLocalMap对象
//2) 调用createMap进行ThreadLocalMap对象的初始化
//3) 并将当前线程t和value作为第一个entry存放至ThreadLocalMap中
this.createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal)this);
}
//返回设置的value
return value;
}
执行流程:
1)首先获取当前线程,并根据当前线程获取ThreadLocalMap
2)如果map不为空,则根据当前ThreadLocal获取对应的实体e;如果map为空,则跳转到第4步
3)如果实体e不为空,那么返回e对应的value值,否则跳转到第4步
4)调用initialValue()
方法,获取初始值value,然后使用当前ThreadLocal和初始值value初始化当前线程维护的ThreadLocalMap。然后返回初始值value。
总结:先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回初始值。
public void remove() {
//获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocal.ThreadLocalMap m = this.getMap(Thread.currentThread());
//如果map存在
if (m != null) {
//调用map.remove,删除key为当前ThreadLocal的记录
m.remove(this);
}
}
执行流程:
1)首先获取当前线程,并根据当前线程获取ThreadLocalMap
2)如果map不为空,则移除当前ThreadLocal对象对应的entry
/*
返回当前线程对应ThreadLocal的初始值
此方法的第一次调用发生在:当前线程通过get方法调用此线程的ThreadLocal值时
如果先调用了set方法,那么该方法将不会被调用
通常情况下,每个线程最多调用一次这个方法
*/
protected T initialValue() {
return null;
}
set
方法就调用get
方法时,该方法才会被调用,且只被调用一次null
null
之外的初始值,可以重写这个方法。ThreadLocalMap
是ThreadLocal
的内部类,没有实现Map
接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现的,它们的类图如图所示:
(1)成员变量
//初始容量 -- 必须是2的整次幂
private static final int INITIAL_CAPACITY = 16;
//存放数据的table
private ThreadLocal.ThreadLocalMap.Entry[] table;
//数组里面entrys的个数,用于判断是否超过阈值
private int size = 0;
//进行扩容的阈值
private int threshold;//Default to 0
(2) 存储结构 - Entry
/*
Entry继承WeakReference,并且用ThreadLocal作为Key
如果key为null(entry.get()==null),意味着key不再被引用
因此这时候entry也可以从table中清除
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
//调用WeakReference的构造函数
//意味着一个entry对象就是一个ThreadLocal的弱引用
super(k);
this.value = v;
}
}
Entry
继承WeakReference
,也就是key(ThreadLocal
)的一个弱引用,其目的是将ThreadLocal
对象的生命周期和线程的生命周期解绑。
(1)内存泄露相关概念
(2) 弱引用相关概念
Java中的引用有4中类型:强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:
强引用(Strong Reference),就是我们最常见的普通对象引用,只要还有强引用指向对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
弱引用(WeakReference),垃圾回收器一旦发现只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。
(3)如果key使用强引用
堆栈中的引用关系如图所示:
假设业务中使用完ThreadLocal,threadLocalRef被回收了(可能是方法调用结束,栈帧出栈),此时堆栈中的引用关系如下图:
但是如果线程还没有结束,因为ThreadLocalMap
的Entry
强引用了ThreadLocal
,所以导致ThreadLocal
无法被回收,从而导致内存泄漏。
如果Entry
是一个对ThreadLocal
的弱引用,那么在threadLocalRef被回收后,堆中的ThreadLocal
对象可以被回收掉,因为只有弱引用指向它了。被回收后堆栈中的引用关系如图:
(4)真实内存泄漏原因
如上图所示,当ThreadLocal
对象被回收之后,如果线程还没有结束,那么该线程中ThreadLocalMap
中对应于该ThreadLocal
的Entry
的key的值变为null
,导致整个value都无法被访问到,因此任然存在内存泄漏(即My Value 内存块无法被回收)。
为了避免这种类型的内存泄露,正确的做法是:
使用完ThreadLocal
,调用其remove
方法删除对应的Entry
使用完ThreadLocal
,当前线程也随之结束。
ThreadLocalMap
中hash冲突的解决在ThreadLocal
的set
方法中,如果当前线程的ThreadLocalMap
不存在,就会创建当前线程的ThreadLocalMap
。
(1)构造方法:
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16;
private ThreadLocal.ThreadLocalMap.Entry[] table;
private int size = 0;
private int threshold;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table
this.table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//计算索引,重点
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1);
//设置值
this.table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
this.size = 1;
//设置阈值,初始容量的2/3
this.setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
this.threshold = len * 2 / 3;
}
}
构造函数首先创建一个长度为16的Entry
数组,然后计算firstKey
对应的索引,然后存储到table
中,并设置size
和threshold
重点分析:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1);
关于firstKey.threadLocalHashCode
:
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88947;
private static int nextHashCode() {
//获取当前值,并加上HASH_INCREMENT
//按照ThreadLocalMap的创建顺序,其threadLocalHashCode的值依次为:
//0,HASH_INCREMENT,2*HASH_INCREMENT,3*HASH_INCREMENT....
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
HASH_INCREMENT = 0x61c88947
这个数跟*斐波那契数列(黄金分割数)*有关,其主要目的就是为了让哈希码能够均匀的分布在2的n次方的数组里。这样可以尽量避免hash冲突。
关于& (INITIAL_CAPACITY-1)
计算索引的时候采用hashCode & (size-1)的算法,这相当于取模运算hashCode%size的一个更加高效的实现。正因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,是的hash发生冲突的次数减小。
(2)set
方法:
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = this.table;
int len = tab.length;
//计算索引
int i = key.threadLocalHashCode & len - 1;
/*
使用线性探测法解决hash冲突
*/
for(ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = (ThreadLocal)e.get();
//ThreadLocal对应的key存在,直接覆盖之前的值
if (k == key) {
e.value = value;
return;
}
//key为null,说明之前的ThreadLocal对象已经被回收了
//tab[i]指向的Entry是一个陈旧的元素
if (k == null) {
//用新元素替换旧的元素,这个方法进行了不少垃圾清理动作,防止内存泄露
this.replaceStaleEntry(key, value, i);
return;
}
}
//在线性探测的过程中,如果没有发生hash冲突:
//则直接在key.threadLocalHashCode & len - 1下标下存放新的Entry
//在线性探测的过程中,发生hash冲突,但是冲突的位置下存放的entry都是正常的(原key不为空且不等于新key):
//将会在tab的一个空闲下标下存放新的Entry
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++this.size;
//cleanSomeSlots(i, sz)用于清理那些e.get()==null的元素(非全表清理)
//如果没有清理任何entry,并且当前tab的使用量达到阈值,那么进行rehash
//rehash中进行一次全表清理
//如果清理之后tab中entry个数大于等于threadshold-threshold / 4(即本次清理的个数小于阈值的四分之一)
//将tab扩容到原来的2倍
if (!this.cleanSomeSlots(i, sz) && sz >= this.threshold) {
this.rehash();
}
}