一、什么是ThreadLocal
我们先看一下API说明(1.8版本)
翻译过来就是
此类提供线程局部变量。 这些变量与普通变量不同,每个使用该变量(通过其get或set方法)的线程都会初始化一个完全独立的实例副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
总的来说:ThreadLocal 与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal 是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以说ThreadLocal 为多线程环境下变量问题提供了另外一种解决思路,实质就是一种“空间换时间”的思想。
二、为什么要使用ThreadLocal
从上面的的定义我们知道ThreadLocal 就是定义了线程的本地变量,那么这样做有什么样的好处呢?我们知道如果我们想要在方法内使用到方法外的变量(不包括当前类或父类中的成员属性),我们一般会采用如下的方式
- 1)方法传参
- 2)将需要使用的变量定义为类的静态变量
上面两种方式虽然可行,但是有其弊端。如果方法的调用层级很深入,给方法加上参数传递会导致其余方法也需要修改,而且可能很多地方会使用到这个方法,修改的成本过大且容易出错;使用类的静态变量会导致别的线程也可以访问到该变量,可能会导致线程安全问题。
因此Java提供了ThreadLocal ,来表示线程的本地变量,线程间互不影响,每个线程在任何地方都可以取到该变量,省去了参数传递的麻烦,同时也保证了线程安全的问题。
三、ThreadLocal用法
举个简单的小例子(这个例子可能会不能明显地体现ThreadLocal的优势,但是讲解如何使用已经足够了)
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
for (int i = 1; i <= 5; i++) {
int j = i;
new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println("设置" + name + "的threadLocal为:" + name);
threadLocal.set(name);
System.out.println(">>>取出" + name + "的threadLocal:" + threadLocal.get());
}, i + "号线程").start();
}
}
执行结果
设置1号线程的threadLocal为:1号线程
设置5号线程的threadLocal为:5号线程
>>>取出5号线程的threadLocal:5号线程
设置4号线程的threadLocal为:4号线程
设置3号线程的threadLocal为:3号线程
设置2号线程的threadLocal为:2号线程
>>>取出2号线程的threadLocal:2号线程
>>>取出3号线程的threadLocal:3号线程
>>>取出4号线程的threadLocal:4号线程
>>>取出1号线程的threadLocal:1号线程
ThreadLocal的用法其实非常简单,只需要实例化一个实例,调用set方法设置本地变量,get方法获取到本地变量。
四、ThreadLocal原理
1. ThreadLocalMap
在介绍原理之前,我们先介绍一下ThreadLocal的一个内部类——ThreadLocalMap
1.1 属性
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold;
属性类似hashmap,只不过这里固定数组的类型为Entry,这个Entry是它的内部类,定义如下
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry类继承WeakReference(表示弱引用),它的构造方法中将ThreadLocal以弱引用的方式保存。
1.2 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
1.3 核心方法
-
保存键值 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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
-
获取Entry对象 private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
当在指定位置找不到对应的Entry时,因为可能存在hash冲突,会采用开放地址法解决冲突,所以需要进一步查找数据,方法定义如下 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
-
移除指定Entry 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;
}
}
}
2. 存值
ThreadLocal提供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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
我们可以看到,每个Thread类中都存在一个ThreadLocalMap属性,我们要存在的值就存在该map中的Entry节点的value属性中,我们可以通过ThreadLocal作为key去找到Entry节点
ThreadLocal.ThreadLocalMap threadLocals = null;
3. 取值
ThreadLocal提供set方法来返回当前线程所对应的线程变量
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();
}
4. 移除值
ThreadLocal提供remove方法移除当前 ThreadLocal 对应的值
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
五、ThreadLocal的内存泄漏问题
我们来看一下ThreadLocal 的对象关系引用图(其中实线表示强引用)
我们知道弱引用是为了利于GC回收的,如果一个对象只有弱引用,GC就会回收这个对象。当JVM栈中的ThreadLocal对象的引用为null时,ThreadLocal对象就只剩下弱引用了,此时就会回收ThreadLocal对象,同时Entry中的key对象也会置为空,但是Entry中的value不会被回收,因为根据可达性分析,value存在这样一条链路:Thread对象引用->Thread对象->ThreadLocalMap对象->Entry对象->Value对象,因此value不会被回收,与线程同生命周期,这样就存在了内存泄漏,尤其在使用线程池的时候。因此在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情况,会对value设置为null。当然我们也可以显示调用ThreadLocal的remove()方法进行处理。所以实际上从ThreadLocal 设计角度来说是不会导致内存泄露的!
六、总结
ThreadLocal 提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程ThreadLocal 的设计是:每个Thread 维护一个ThreadLocalMap 哈希表,这个哈希表的key 是ThreadLocal 实例本身,value 才是真正要存储的值Object 。- 对
ThreadLocal 的常用操作实际是对线程Thread 中的ThreadLocalMap 进行操作。 ThreadLocalMap 中的哈希表Entry[] table 存储的核心元素是Entry ,存储的key 是ThreadLocal 实例对象,value 是ThreadLocal 对应储存的值value 。需要注意的是,此Entry 继承了弱引用 WeakReference ,所以在使用ThreadLocalMap 时,发现key == null ,则意味着此key ThreadLocal 不在被引用,需要将其从ThreadLocalMap 哈希表中移除。ThreadLocalMap 使用ThreadLocal 的弱引用作为key ,如果一个ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal 势必会被回收。所以,在ThreadLocal 的get() ,set() ,remove() 的时候都会清除线程ThreadLocalMap 里所有key 为null 的value 。如果我们不主动调用上述操作,则会导致内存泄露。- 为了安全地使用
ThreadLocal ,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal 后都要调用remove() 来清理无用的Entry 。这在操作在使用线程池时尤为重要。
|