ThreadLocal,我们依然从一个例子看起。
例子
public class ThreadLocalTest2 {
public static void main(String[] args) {
// ①,强引用tl1
ThreadLocal tl1 = new ThreadLocal();
tl1.set(11);
System.gc();
tl1.get();
System.gc();
// tl1.remove();
// ②,没有强引用
new ThreadLocal<>().set(22);
System.gc();
// ③,方法里的tl
ThreadLocalTest2 test2 = new ThreadLocalTest2();
test2.test1();
System.gc();
Thread thread = Thread.currentThread();
System.out.println(thread);
}
void test1() {
ThreadLocal<Integer> tl = new ThreadLocal<>();
tl.set(33);
// tl.remove();
}
}
debug模式下,我们看下thread里ThreadLocalMap里的情况,如下图。
①因为存在强引用,所以对应的referent不为null。
②没有强引用,垃圾回收后,referent为null。
③ThreadLocal仅在test1()方法中有效,所以垃圾回收后,referent为null。
ThreadLocal源码解析
ThreadLocal类结构
public class ThreadLocal<T> {
...
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;
}
}
...
}
...
}
从线程到ThreadLocal.set(value)的value的引用关系就是这样的
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
set(T value)
设置线程本地变量的内容。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get()
获取线程本地变量的value值
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();
}
- initialValue()
设置 ThreadLocal 的初始化值,未 set(T value) 初次获取 Thread 对应的 Value 值时会调用,即被 setInitialValue 方法调用。需要重写该方法。
protected T initialValue() {
return null;
}
remove()
移除线程本地变量。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal使用完后,一定要调用remove()!!!
不手动调用remove()方法,
1.容易发生内存泄漏。
2.线程池场景下后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
几个问题
ThreadLocalMap的KEY为什么设计成弱引用
- 如果设计成强引用,即使我们使用完ThreadLocal,只要线程没有结束,线程对应的ThreadLocal就不会被回收。这个时候当ThreadLocal太多的时候就会出现内存泄漏的问题。
- 设计为弱引用后,只要没有了强引用,jvm垃圾回收时,便会将key(ThreadLocal)进行回收。
降低内存泄漏的风险。
ThreadLocalMap的VALUE为什么不设计成弱引用
这里也用一个例子,来说明value为什么不是弱引用
public class TLLeakTest {
public static void main(String[] args) {
// ①,key和value均为WeakReference
Map<WeakReference<Integer>, WeakReference<Integer>> map = new HashMap<>(8);
WeakReference<Integer> key = new WeakReference<>(1);
WeakReference<Integer> value = new WeakReference<>(777);
map.put(key,value);
System.gc();
System.out.println("get1: " + map.get(key).get());
// ②,key为WeakReference,value为HardReference
Map<WeakReference<Integer>, Integer> map2 = new HashMap<>(8);
WeakReference<Integer> key2 = new WeakReference<>(1);
map2.put(key2, 999);
System.gc();
System.out.println("get2: "+ map2.get(key2));
}
}
get1: null
get2: 999
以上结果,可以说明为什么value不能为弱引用。
如果value也为弱引用,那么得到的结果会为null。
ThreadLocal为什么内存泄露
来看例子。
- 单线程模式
public class ThreadLocalLeakTest {
public static void main(String[] args) throws InterruptedException {
ThreadLocal<byte[]> local = new ThreadLocal<>();
local.set(new byte[1024 * 1024 * 60]);
Thread.sleep(100);
local = null;
while (true) {
TimeUnit.SECONDS.sleep(1);
}
}
}
从上图的堆内存使用图可以看出,无论执行多少次GC,内存使用量总是在60M以上,即ThreadLocalMap中value的60M空间始终无法释放。
- 线程池模式
– ThreadLocal不remove()
public class ThreadLocalLeakTest2 {
static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
static ThreadLocal<byte[]> local = new ThreadLocal();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 500; i++) {
executor.execute(() -> {
local.set(new byte[1024 * 1024 * 5]);
System.out.println(Thread.currentThread().getName());
});
}
local = null;
}
}
从上图看出,这里无论执行多少次GC,堆内存依然会有25M左右的使用量,即5个线程 * 5M空间 = 25M。
使用完ThreadLocal,如果没有及时remove(),那么key因为是弱引用就会被回收,但value是强引用,value没被回收,导致value永远存在,造成内存泄漏。