这是我参与新手入门的第1篇文章
初识ThreadLocal
我们先通过一段代码来认识一下 ThradLocal
主程序
package com.example.threadlocal_01.com.yang;
/**
* static修饰 类加载的时候创建ThreadLocal对象 tl,并且所有对象共享
* 线程一:通过set() 设置了一个张三的Person对象 打印输出
* 线程二:通过get() 去获取tl对象 打印输出
* 为了排除线程执行速度的影响,线程一睡眠1s,线程二睡眠2s
*/
public class ThreadLocalDemo01 {
static ThreadLocal<Person> tl = new ThreadLocal<>();
public static void main(String[] args) {
// 线程一
new Thread(() -> {
SleepHelper.sleep(1);
tl.set(new Person("张三"));
System.out.println(Thread.currentThread().getName() +":"+ tl.get());
}).start();
// 线程二
new Thread(() -> {
SleepHelper.sleep(2);
System.out.println(Thread.currentThread().getName() +":"+ tl.get());
}).start();
}
}
相关类
- SleepHelper
public class SleepHelper {
public static void sleep(int seconds) {
try {
Thread.sleep(1000L * seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- Person
@Data
@AllArgsConstructor
public class Person {
String name;
}
在执行代码之前,根据以往的经验,我们可以预测一下输出结果,带着答案去看结果,可以加深我们的印象。
- 输出结果
Thread-0:Person(name=张三)
Thread-1:null
结果和我想的好像不太一样,在共享的 tl 对象里设置了值,第二个线程里确没有获取到。
总结
查阅资料,ThreadLocal可以理解为当前线程的一个全局变量,只作用于当前线程,所以线程一里的tl设置了值,线程二里获取不到,从定义上来说似乎可以解释的通,但是这tl必定是一个共享对象,这又和常理相悖,带着疑问,决定追看一下源码。
ThreadLocal源码探索
set()方法
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 传入当前线程对象,获取一个ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果map不为空,以this作为key,Person作为value,set进这个map,如果为空则创建一个
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
这里的this指的是调用set方法的tl对象
getMap(t)
ThreadLocalMap getMap(Thread t) {
// 返回当前线程的 threadLocals
return t.threadLocals;
}
threadLocals
// Thread 对象的成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;
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();
}
getMap(t)
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
看到这我们知道, 任何一个Thread对象里带有一个Map类型的成员变量threadLocals,之前的Person对象存在了这里,所以其它线程肯定是获取不到此线程threadLocals里的值的.
使用ThreadLocal时注意项
先来了解两个概念:
内存泄漏:内存被占用,一直无法被释放
内存溢出:内存不断被消耗,最后内存不够用导致溢出
内存泄漏可能会导致内存溢出,但是内存泄漏不等于内存溢出
- ThreadLocal使用可能会导致内存泄漏
当线程使用了ThreadLocal,如果线程一直存在,那么这个map占用的内存就永远不会被释放掉,就会导致内存泄漏,如果一直不做处理,久而久之最后就会造成严重的资源浪费,最终导致内存溢出。
所以在我们使用ThreadLocal的时候应该手动去清空map,让GC可以发现并回收内存。
remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
线程池中使用ThreadLocal
- 内存泄漏
- 数据不一致
说到线程不死,很自然想到了线程池,当一个线程池中使用了ThreadLocal,如果使用完我们不主动去remove(),就存在内存泄漏的情况。
我们知道线程池的特性就是一次性创建多个线程,线程不销毁重复利用,那么就会造成,我上一次使用时map里设置的值没有清除,下一次其它方法使用的时候直接拿到的是上一次的值,就会出现数据混乱的情况。
扩展
ThreadLocalMap里的Entry对象
弱引用:GC发现就会被回收掉
Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我们发现这里的Entry继承自一个弱引用。这里发现了一个有趣的事,言语难以表达,看图
为什么这里使用弱引用,若果使用强引用,即使tl对象变成null了但是map里key的引用依然指向ThreaLocal对象,所以依然会造成内存泄漏。就算ThreadLocal对象被回收了,key变成了null,导致了整个value对象再也无法被访问到,因此依然会有内存泄漏的情况,而使用弱引用则不会出现这种情况。
写在最后的话
最后,说一下写这篇文章的心路历程吧!
在决定写这个题目的时候,也看了很多大佬的文章,写的过程中也是很忐忑,怕犯一些低级错误,误导了看到这篇文章的朋友。所以在写的时候也格外仔细,写的过程中会发现,一个简单的知识点,理解很简单,但是要表达出来,让看的人懂,似乎不是件简单的事。在学习的道路上还需要砥砺前行。
如有纰漏,请各位大佬指正!