ThreadLocal是一个泛型类,作用是实现线程隔离,ThreadLocal类型的变量,在每个线程中都会对应一个具体对象,对象类型需要在声明ThreadLocal变量时指定。
ThreadLocal的使用示例
package org.example.thread;
import org.example.domain.Book;
public class ThreadLocalTest {
ThreadLocal<Book> localBook = new ThreadLocal<>();
Book book = new Book();
public void test() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
localBook.set(book);
System.out.println(localBook.get());
localBook.remove();
}
});
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(localBook.get());
localBook.remove();
}
public static void main(String[] arg) {
ThreadLocalTest localBookTest = new ThreadLocalTest();
localBookTest.test();
}
}
运行结果如下:
示例代码中,只声明了一个 ThreadLocal 变量localBook,在子线程中设置了localBook的值,但是在最后主线程中进行打印时,发现为null,和子线程中的结果不一样。具体原因还要看看ThreadLocal类,以及内部 set、get等方法的实现。
原理
1、ThreadLocal中有一个静态内部类 ThreadLocalMap,ThreadLocalMap中维护了一个 Entry数组,Entry又是ThreadLocalMap的内部类,用来表示一个KV键值对。部分源码如下:
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;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
Entry的 key是一个指向ThreadLocal对象的弱引用,value则指向与key对应的Object对象。
2、Thread类中,有一个ThreadLocalMap类型的成员变量,初始为null:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
也就是说,每个线程对象中,都有一个ThreadLocalMap,它里面可以有很多个Entry,每个Entry中的key都指向一个ThreadLocal对象,value则指向与key相对应的Object对象。在线程中调用ThreadLocal对象的set、get方法,实际操作的是当前线程自己的ThreadLocalMap。
3、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);
}
果然,这里先拿到当前线程中的ThreadLocalMap,如果不为空则直接添加一组键值对,key是当前的ThreadLocal变量;
如果map为空,则新建一个map,同样添加一组键值对,key是当前的ThreadLocal变量,然后将map赋值给当前线程的ThreadLocalMap。
4、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方法拿到的也是线程本地的ThreadLocalMap中,与当前ThreadLocal变量对应的value对象。
5、remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
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;
}
}
}
remove方法,移除当前线程的ThreadLocalMap中,与当前ThreadLocal对象对应的键值对(key、value及Entry置空)。
内存泄漏相关
1、为什么ThreadLocalMap中的key要使用弱引用?
弱引用特性:在对象没有被强引用指向,而仅被弱引用指向的情况下,发生垃圾回收时,会直接清理掉该对象。
套用网上出现较多的一张图片来表示ThreadLocal变量的内存结构:
这里实线表示强引用,虚线表示弱引用。
假设key也是强引用,如果我们把 ThreadLocal Ref这个引用置为null,表示我们不再需要这个ThreadLocal对象,那么发生垃圾回收时,这个变量理应被回收掉;但实际并非如此,因为当前线程中还持有一个强引用,也就是ThreadLocalMap中的key还在指向它,那么只要线程不结束,这个没有实际作用的ThreadLocal对象就一直不会被回收,从而出现内存泄漏。而将key设计为弱引用,就能保证这种情况下ThreadLocal对象被回收,一定程度避免内存泄漏问题。
2、为什么使用完ThreadLocal对象,要在线程中调用remove方法?
虽然弱引用可以使不再使用的ThreadLocal对象被回收掉,但还有一个问题:
Entry中的key被置为了null,对应的value已经无法通过key访问到,然而Current thread -> ThreadLocalMap -> Entry(value) -> my value 这条强引用链仍然存在,也就是说还存在key为null,但value不为null的entry,如果这类entry不被清理掉,还是会导致内存泄露。
key为null的entry如何清理?
在ThreadLocalMap的Entry数组中,key为null的entry也会占用一个数组下标,而Threadlocal的set、get等方法,正是根据key的hashCode计算得到数组下标,然后根据下标找到对应的entry进行操作。
为了维护ThreadLocalMap的可用性,不让这些key为null的无用entry占用过多空间,set、get方法在某些情况下也会对key为null的entry进行清理。但是用户有可能不再需要调用其他ThreadLocal变量的set或get方法,所以这种方式是被动且没有针对性的。
建议在不使用ThreadLocal对象之后,直接调用remove方法,其作用就是将对应entry的key、value置空,并将entry从数组中移除,切断下图中的这三处引用关系,防止内存泄漏。