面试分享~No.3
要解决的问题
在多线程的场景下,使用共享变量,线程无法隔离单独的使用这个变量
threadlocal可以让每个线程独享一份共享变量,实现共享变量的隔离
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class ThreadLocalTest {
private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
public static void main(String[] args) { new Thread(() -> { threadLocal1.set("local1 A"); threadLocal2.set("local2 A"); System.out.println(threadLocal1.get()); System.out.println(threadLocal2.get()); threadLocal1.remove(); threadLocal2.remove(); }).start();
new Thread(() -> { threadLocal1.set("local1 B"); threadLocal2.set("local2 B"); System.out.println(threadLocal1.get()); System.out.println(threadLocal2.get()); threadLocal1.remove(); threadLocal2.remove(); }).start(); }
}
|
源码分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| 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); }
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); }
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(); }
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
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; }
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
|
ThreadLocalMap
1 2 3 4 5 6 7 8 9 10 11 12
| static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> { Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } }
|
为什么Entry使用弱引用类型?
Thread 与
ThreadLocal、ThreadLocalMap的关系
JDK8之前的设计
JDK8的设计
JDK8的设计说明:
- 每个Thread对象中有一个ThreadLocalMap属性
- ThreadLocalMap是一个字典,key是ThreadLocal类型,value是要保存的变量
- Thread中没有提供操作ThreadLocalMap的方法,而是通过ThreadLocal操作的
问题:
JDK8之前的设计存在什么问题
答:个人理解,Threadlocal 是为了将 Thread 和 value 进行关联,那么
value 的生命周期应该与 Thread 保持一致,当 Thread 销毁了,value
当然就不需要了。在JDK8之前,thread 是 map 的 key,当 thread
销毁时,value 依旧是存在的,在 JDK8 之后,value 在 thread 内的
threadLocalMap 中,当 thread 销毁后,value 会自动销毁。另外,JDK8
之前如果 threadlocal 对象销毁,则其下的所有 thread 和 value
的关系都不在存在了,但是 thread
对象可能依旧是存在的,这也是不合适的
ThreadLocalMap做为Thread的属性,为什么不通过Thread对象来操作
答:threadLocalMap 的 key 必须是 threadLocal 对象,如果允许通过
thread 的方法操作,可能传入其他对象作为 key,破坏了设计思想
应用场景
- 存储全局信息,如用户Session
- 处理数据库事务
- 代替参数显式传递
ThreadLocal中内存泄漏的问题
内存泄露:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费
弱引用:执行垃圾回收时,不管符不符合回收条件,都会回收
实验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit;
public class ThreadLocalTest { static class LocalVariable { private byte[] arr = new byte[1024 * 1024 * 5]; }
private final static ThreadPoolExecutor executor = new ThreadPoolExecutor(6, 6, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
static ThreadLocal<LocalVariable> tl = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { executor.execute(() -> { LocalVariable localVariable = new LocalVariable(); ThreadLocalTest.tl.set(localVariable); System.out.println("thread name end: " + Thread.currentThread().getName() + ", value: " + ThreadLocalTest.tl.get());
}); Thread.sleep(1000); } Thread.sleep(1000 * 3600); }
}
|
执行GC,存在30M内存泄漏
将threadlocal remove掉,GC后没有内存泄漏
1
| ThreadLocalTest.tl.remove();
|
引用关系分析
Threadlocal
会产生内存泄漏吗?
前提:启动一个线程执行很多个计算任务,线程一直工作不会被销毁
1 2 3 4 5 6 7 8 9
| static class Entry extends WeakReference<ThreadLocal<?>> { Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
|
threadlocal的引用有:
- 创建threadlocal对象的强引用 tl
- threadLocalMap中的弱引用key
object 的引用:
- threadlocalMap中的强引用value
当tl=null时,threadlocal可以被回收;当value=null时,object可以被回收
讨论
假设我们线程池中有100个线程,和一个 threadLocal
对象,我们用这100个线程执行100个数据任务对象(10M),并将数据任务通过
threadLocal set 到线程内的 threadLocalMap 中,这样每个线程中,都有一个
map,map
中有一个长度为16的数组,数组中有一个元素,即数据任务。执行完所有数据任务后,假设这100个线程都是核心线程,线程池会保持线程,这时,数据任务是有强引用指向的,即
Entry 的 value,不会被回收
有说法称 threadlocal
可能导致内存泄露,这个说法是不对的,顶多可以说是业务上的"内存泄漏",因为在业务层面我们是不需要这份数据的,但这份数据不符合GC的条件,不会被回收,所以需要我们手动清理
这种内存泄露会随着线程数量增大而增大,与线程执行次数无关
Entry 的 key 为什么用弱引用
threadlocal 对象有两个引用,一个是强引用 tl,Entry 中用弱引用指向了
threadlocal,如果代码中令tl=null,则 threadlocal 对象可以在 GC
时被回收
总结
本文介绍了threadlocal的使用场景,用法,注意事项:threadlocal使用后记得remove
参考
ThreadLocal原理及使用场景
ThreadLocal内存泄漏分析