ThreadLocal是以线程隔离为核心,既能解决线程并发下的数据安全问题,又能简化线程内的传递数据的操作。
三个关键词:
线程隔离:TheadLocal为每个线程内部维护了一个独立专属的储存容器,线程间数据互不访问,互不干扰,实现数据私有隔离,保证了数据安全性。
线程并发:在多线程的情况下,共享变量容易出现数据错乱,ThreadLocal给每个线程分配独立的数据副本,无需加锁就能解决并发的安全问题。
传递数据:ThreadLocal存一次数据,当前线程内任意地方可以直接获取,简化了数据的传递逻辑和耦合度。无需逐层传参(Controller->Service->DAO)
为什么要使用ThreadLocal?
场景:在多线程中需要操作共享变量,容易出现数据错乱。
static String name=null;
static int age=0;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
name="张三";
age=21;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程1 "+name+" "+age+"岁");
});
Thread thread2 = new Thread(() -> {
try {
name="李四";
age=22;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程2 "+name+" "+age+"岁");
});
thread2.start();
thread.start();
}
比如上面这个代码,多个线程操作共享变量。
想要的运行结果:
线程1 张三 21岁
线程2 李四 22岁
实际上:

出现了数据错乱。
我们可以通过加锁解决:但是加锁的话,会导致其他问题,比如并发量降低,性能因为是同步执行,性能也会下降。
Thread thread = new Thread(() -> {
synchronized (threadLocal1) {
try {
name="张三";
age=21;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程1 "+name+" "+age+"岁");
}
});
Thread thread2 = new Thread(() -> {
synchronized (threadLocal1) {
try {
name="李四";
age=22;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程2 "+name+" "+age+"岁");
}
});
但是ThreadLocal的出现,很好的解决了这样的问题。
public class Demo {
String name;
int age;
public Demo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Main {
static ThreadLocal<Demo> threadLocal1 = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
threadLocal1.set(new Demo("张三", 21));
Demo demo = threadLocal1.get();
System.out.println("线程1 " + demo.getAge() + "岁 " + demo.getName());
});
Thread thread2 = new Thread(() -> {
threadLocal1.set(new Demo("李四", 22));
Demo demo = threadLocal1.get();
System.out.println("线程2 " + demo.getAge() + "岁 " + demo.getName());
});
thread.start();
thread2.start();
}
}
这样就能很好的解决了数据错乱的问题。
synchronized和ThreadLocal的区别:
synchronized的使用,基于共享数据加锁,让多线程排队访问共享资源。解决线程间的数据竞争问题。是用时间换空间,而且使用了synchronized会使性能下降(串行执行),因为同步执行代码。并发性降低。
ThreadLocal的使用,基于数据副本隔离,给每个线程分配独立的数据副本。避免线程间共享资源。是用空间换时间,使用ThreadLocal是不会影响并发性的。
ThreadLocal内部结构:

每个线程在首次调用了ThreadLocal的set/get方法后才会创建ThreadLocalMap,随线程的生命周期存在。
ThreadLocalMap中,会以当前的ThreadLocal实例作为key,以及与之对应的value,放入其中。
ThreadLocal的核心方法源码:
1.set(数据):
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
set设置当前线程绑定的局部变量。
1.首先先获取当前线程,并根据当前线程获取一个Map
2.如果Map不为空,则将参数设置到Map中(key=ThreadLocal实例本身)
3.如果Map为空,就会为当前线程重新创建一个Map,并设置初始值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
2.get(数据):
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
get是获取当前线程绑定的局部变量。
1.首先获取到当前线程,然后根据线程获取到Map
2.如果Map不为空,则根据ThreadLocal作为引用来获取Entry e,否则就会转到第四步
3.若e不存在,就会执行第四步
4.通过setInitialValue()方法,函数获取初始值,然后用ThreadLocal引用和对应value来创建新的Map
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);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
3.remove(数据):
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
remove方法是移除当前线程所绑定的局部变量。
1.首先根据当前线程,获取到Map
2.如果Map不为空,则直接移除。
ThreadLocalMap:

ThreadLocalMap是ThreadLocal的底层存储容器,也是ThreadLocal的内部类。专属线程私有,核心作用是隔离线程间的数据。
本质是一个弱化版的哈希表,规避线程安全问题的同时提升储存效率。
ThreadLocalMap源码分析:
在代码中,有几个很重要的变量:

储存结构-Entry:

在ThreadLocalMap中,也是用Entry来保存K-V结构的数据,不过Entry中的key只能用ThreadLocal对象。
另外,Entry继承了继承了WeakReference,也就是key(ThreadLocal)是弱引用,目的是将ThreadLocal对象的生命周期和线程的生命周期解绑。
弱引用和内存泄漏:
有的人认为,内存泄漏的原因是因为使用了弱引用,这是不正确的。
下面我分别从强引用和弱引用来阐述产生内存泄漏的原因:
1.使用强引用:
如图:

1.假设业务代码在使用完了TheaLocal,然后ThreadLocal Ref被回收了。
2.但是因为ThreadLocalMap的Entry强引用了ThreadLocal,造成ThreadLocal无法被回收。
3.在没有手动删除这个Entry以及CurrenThread依然运行状态下(线程池场景下),始终就会有强引用链Thread Ref->currentThread->threadLocalMap->entry,entry不会被回收(entry包括了ThreadLocal实例和value),导致内存泄漏。
2.使用弱引用:

1.当业务代码中使用完ThreadLocal Ref,ThreadLocal Ref就会被GC回收。
2.由于ThreadLocalMap中的entry只持有ThreadLocal当弱引用,所以ThreadLocal也会被GC回收,所以此时Entry中的key就会变成null。
3.但是如果没有手动删除这个Entry以及CurrentThread依然运行状态下(线程池场景),也存在强引用链:CurrentThread Ref->CurrentThread->ThreadLocalMap->entry->value,value不会被回收,而这块地址也找不到了,导致value内存泄漏。
ThreadLocalMap核心方法set:
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)]) {
if (e.refersTo(key)) {
e.value = value;
return;
}
if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
首先ThreadLocalMap是用线性探测法,而且数组还是环形的,保证哈希冲突继续寻找。
ThreadLocal采用弱引用,避免内存泄漏(会被GC回收)。
核心逻辑:插入 / 替换值时,先通过哈希定位初始索引,线性探测遍历;匹配到目标 Entry 则替换值,遇到过期 Entry 则清理并插入(避免弱引用导致的内存泄漏,且没有remove的兜底操作),遍历到空位置则直接插入。
插入后主动清理部分过期 Entry,负载因子过高时扩容,全量清理过期 Entry。



1704

被折叠的 条评论
为什么被折叠?



