前言
ThreadLocal工作中会偶有用到,是解决thread间的数据隔离问题的(并不是为解决并发和共享问题的),也是面试常见问题,比如:ThreadLocal知道吗?说说你自己的理解?或者这样问:在多线程环境下,如何防止自己的变量被其它线程篡改?无论基于哪种原因都是很有必要学习的。
ThreadLocal是什么
贴一段源码中的介绍:这个类提供线程的局部变量,可以通过get()和set()方法来获取和设置自己的局部变量;ThreadLocal实例通常是pricate static fields的,希望将信息关联到一个线程中,例如:user ID、Transaction ID
概括的说:存储各个线程互不相同的信息,实现线程间的数据隔离。
/**
* 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).
*/
ThreadLocal能做什么
-
实例1:参数传递(用户信息传递)
当前用户信息需要被线程内的所有方法共享
-
方案1:传递参数
将user作为参数在每个方法中进行传递,缺点:会产生代码冗余问题,并且可维护性差
-
方案2:使用Map
缺点:如果在单线程环境下可以保证安全,但是在多线程环境下是不可以的。如果使用加锁或是ConcurrentHashMap都会产生性能问题。
-
方案3:使用ThreadLocal,实现不同方法间的资源共享
-
public class ThreadLocalNormalUsage02 {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
public void process() {
User user = new User("张三");
//将User对象存储到 holder 中
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用户名: " + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名: " + user.name);
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
结果:
总结:实际项目中也会用到这个实例,在请求的拦截器中添加用户信息到ThreadLocal< User >的ThreadLocalMap中,这样同一线程中的各个方法或组建就可以获取到对应的用户信息。
- 实例2:典型工具类(SimpleDateFormat和Random)
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
//创建一份 SimpleDateFormat 对象
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
ThreadLocal为每个线程创建独立的SimpleDateFormat对象(实际应用中,针对不通的format形式,会创建不同的方法,方法中会是一个新SimpleDateFormat对象),当然也可以使用LocalDateTime。
注:可参考这篇博客——地址
ThreadLocal原理
- 首先看set()方法:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap,这个map是什么那?下面有介绍
ThreadLocalMap map = getMap(t);
if (map != null)
//如果map存在,则以当前线程t为key,数据为value放到map
map.set(this, value);
else
//否则创建新的map再存放数据
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
再来看一下ThreadLocalMap
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
//map的其他定义
}
ThreadLocalMap实际上是ThreadLocal的一个静态内部类,数据就存放在Entry中;
那这个map如何从Thread中获取的那?看下面getMap()和Thread类中的代码
//ThreadLocal中的getMap方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* Thread的threadLocals
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
实际上Thread类中维护了一个ThreadLocalMap变量。
如果map不存在,则创建一个,源码如下:
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 大致了解set的过程,再来看get过程就比较容易理解了
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
//获取线程对象t
Thread t = Thread.currentThread();
//在对象t中获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//从Entry中获取数据
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
如果没有获取map(可能没有set值),则初始化value值
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;
}
protected T initialValue() {
return null;
}
实际上value是null。
- 删除数据:
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();
expungeStaleEntry(i);
return;
}
}
}
- 总结:ThreadLocal实现数据隔离主要是依赖ThreadLocalMap,而每一个线程中都有一个ThreadLocal.ThreadLocalMap变量,这样在set和get数据时,都是获取对应线程中的ThreadLocalMap,数据则存在map里面的Entry中,key为Threalocal对象。
关于ThreadLocal内存泄漏
-
内存泄漏的原因:
- 个人认为ThreadLocal内存泄漏只是有可能,是个小概率事件
- 内存泄露:某个对象不会再被使用,但是该对象的内存却无法被收回
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { //调用父类,父类是一个弱引用 super(k); //强引用 value = v; } }
-
强引用:当内存不足时触发GC,宁愿抛出OOM也不会回收强引用的内存
-
弱引用:触发GC后便会回收弱引用的内存
-
分析:
- 正常情况:当Thread运行结束后,ThreadLocal中的value会被回收,因为没有任何强引用了
- 非正常情况:当Thread一直在运行始终不结束,强引用就不会被回收,存在以下调用链——Thread–>ThreadLocalMap–>Entry(key为null)–>value;因为调用链中的 value 和 Thread,存在强引用,所以value无法被回收,就有可能出现OOM。
JDK的设计已经考虑到了这个问题,所以在set()、remove()、resize()方法中会扫描到key为null的Entry,并且把对应的value设置为null,这样value对象就可以被回收。
-
如何避免内存泄漏:
调用remove()方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal后,要调用remove()方法。
NPE问题
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
public Long get() {
return longThreadLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
//如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错
System.out.println("out:" + threadLocalNPE.get());
new Thread(new Runnable() {
@Override
public void run() {
threadLocalNPE.set();
System.out.println("inner:" + threadLocalNPE.get());
}
}).start();
}
}
上面例子出现NPE问题,主要是get方法返回值类型,我们知道在未对ThreadLocalMap进行set值时,如果直接获取,代码会给我们初始化一个null保存到map中;如果上面get方法返回值是基本数据类型,则会出现装箱和拆箱,导致NPE。
共享数据问题
如果在每个线程中ThreadLocal.set()进去的东西本来就是多个线程共享的同一对象,比如static对象,那么多个线程调用ThreadLocal.get()获取的内容还是同一个对象,还是会发生线程安全问题
总结
可以不使用ThreadLocal就不要强行使用:如果在任务数很少的时候,在局部方法中创建对象就可以解决问题,这样就不需要使用ThreadLocal。
优先使用框架的支持,而不是自己创造:例如在Spring框架中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏。
参考博文:
https://juejin.im/post/5e0d8765f265da5d332cde44#heading-5
https://juejin.im/post/5ac2eb52518825555e5e06ee#heading-4
https://juejin.im/post/5e0d8765f265da5d332cde44#heading-13
https://www.jianshu.com/p/6bf1adb775e0
https://www.jianshu.com/p/98b68c97df9b
https://blog.csdn.net/lufeng20/article/details/24314381?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
https://www.jianshu.com/p/377bb840802f