浅谈ThreadLocal
1、ThreadLocal是什么?
在并发情况下,多个线程对一个共享变量的操作往往是非常危险的。为了保证线程安全,我们需要对该共享变量加synchronized锁,确保在同一个时间内,只有一个线程可以对该共享变量进行读写。ThreadLocal在解决线程安全的问题上提供了一种新的思路,即ThreadLocal为每一个线程提供了一份独立的变量副本,来避免多个线程对共享变量访问冲突的问题,在某些情况下,ThreadLocal比synchronized在解决线程安全的方面上更加方便、灵活。
如果我们创建了一个ThreadLocal变量,那么访问该变量的每一个线程都会在自己的工作内存中创建该变量的一个本地副本。那么多个线程同时操作这个变量时,实际上只是在操作自己工作内存中的变量,从而避免线程安全问题。
2、ThreadLocal简单示例
示例中创建了两个子线程,分别设置其ThreadLocal变量并打印出来,代码结尾再设置主线程的ThreadLocal,最后同样再打印出来。
public class ThreadLocalTest {
//创建一个存储Stirng类型变量的ThreadLocal
private static ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("a");
System.out.println("thread1 local:"+threadLocal.get());
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("b");
System.out.println("thread1 local:"+threadLocal.get());
}
});
thread1.start();
thread2.start();
//设置主线程的ThreadLocal
threadLocal.set("main");
//打印主线程的ThreadLocal变量
System.out.println("main local:"+threadLocal.get());
}
}
输出:
可以看出,各个线程内的ThreadLocal互不干扰,每个线程也只能访问自己独有的ThreadLocal变量。可以看得出来,ThreadLocal另辟蹊径,在解决多线程同步的问题下提供了一种不同的思路。
3、ThreadLocal图解
从上面的结构图我们可以看出:
(1)每个Thread内部都有一个ThreadLocalMap,即定制化的HashMap
(2)map的key是某一个ThreadLocal对象,而value是一个共享变量的值
但是,Thread内部的ThreadLocalMap是由ThreadLocal维护的,由ThreadLocal负责向Thread中的ThreadLocalMap中进行set和get、remove等操作。
4、ThreadLocal内部的实现原理
(1)每个Thread内都有2个ThreadLocalMap实例,分别是
threadLocals:每个线程独有的,不可以访问其他线程的threadLocals中的ThreadLocal
inheritableThreadLocals:子类可以访问父类中的ThreadLocal
以下内容均讨论threadLocals
这一点也可以从ThreadLocal的源码中可以看到
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
(2)ThreadLocal的set()方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//返回当前线程内的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//如果map不为空,则直接将(k:当前ThreadLocal实例,v:共享变量)放入进map中
map.set(this, value);
else
//如果map为空,则创建该线程的ThreadLocalMap,并将(k,v)放入进map中
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
(3)ThreadLocal的get()方法
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果map不为空,则获取键为该ThreadLocal对象的entry实例
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//如果该entry不为空,返回共享变量的值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//使用默认值null填充
return setInitialValue();
}
private T setInitialValue() {
//使用默认值null
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;
}
因此某个ThreadLocal的get()方法有可能返回null,此时进行null值判断非常有必要。
(4)ThreadLocal的remove()方法
public void remove() {
//获取该线程内部的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
总结:在每一个线程内部,都有一个ThreadLocalMap类型的threadLocals变量,用来存储线程变量副本。其中ThreadLocalMap是一个定制化的Hashmap,其Entry的构造方式、hash冲突解决方式与HashMap都不同。ThreadLocalMap中的key为ThreadLocal实例,value为线程变量副本。如果线程不消亡,该变量副本就会一直存在,有可能造成内存泄漏,因此在使用完毕后,要手动调用ThreadLocal中的remove()方法删除该线程下的ThreadLocals的变量副本。
5、ThreadLocalMap的数据结构
ThreadLocalMap是ThreadLocal的一个静态内部类,他没有实现Map等任何集合的顶层接口,而是自己实现了一套独立的map功能,内部Entry对象也是独立实现的。
(1)ThreadLocalMap的局部变量
//初始容量,必须是2的倍数
private static final int INITIAL_CAPACITY = 16;
//内部的Entry数组
private Entry[] table;
//Entry数组的长度
private int size = 0;
//Entry数组大于该长度时,则发生扩容操作
private int threshold;
(2)Entry静态内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在ThreadLocalMap中,是用Entry来保存key-value结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。
Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。关于java中的四种引用的具体解释,可以移步我的另外一篇文章java中的四种引用
(3)既然ThreadLocalMap是特殊的HashMap,那么怎么解决hash冲突?
ThreadLocalMap和HashMap的最大的不同在于,他的结构非常简单,没有next指针引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非数组+链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,则效率很低。
所以这里给出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
6、ThreadLocalMap的问题
由于ThreadLocalMap的key是弱引用,而value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
如何避免泄漏
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
参考文章:
1.《Java并发编程之美》-ThreadLocal