ThreadLocal是一个存储线程本地变量的对象,在ThreadLocal中存储的对象在其他线程中是不可见的,本文介绍ThreadLocal的原理。
1、threadLocal使用
有如下代码:
@Slf4j
public class TestThreadLocal {
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(999);
log.info(threadLocal.get().toString());
//使用线程池创建一个线程
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(()->{
log.info(threadLocal.get().toString());//threadLocal.get()将为null
threadLocal.set(888);
log.info(threadLocal.get().toString());
});
log.info(threadLocal.get().toString());
}
}
输出:
2023-03-09 09:03:40.572 [main] INFO -- 999
2023-03-09 09:03:40.598 [main] INFO -- 999
Exception in thread "pool-1-thread-1" java.lang.NullPointerException
at com.iwat.arithmetic.thread.TestThreadLocal.lambda$main$0(TestThreadLocal.java:24)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
从结果中可以看到,在新线程中无法获取到999,祝线程中set的数,只有祝线程能get到,这就保证了变量的线程唯一。
2、原理
原理就要看源码
首先看threadLocal.set(999);
的源码
//ThreadLocal类中
public void set(T value) {
//1.获取当前线程
Thread t = Thread.currentThread();
//2. 获取线程中的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
//线程中的ThreadLocalMap对象不为空,直接set值,this就是当前ThreadLocal对象没,值就是值
map.set(this, value);
else
//线程中的ThreadLocalMap对象为空,创建并set值
createMap(t, value);
}
很简单,获取当前线程,然后获取当前线程的ThreadLocalMap对象,这里的ThreadLocalMap可以就当做一个Map处理,但是这个Map不一样的地方在于key只能是ThreadLocal对象。现在我们就可以分析一下了。
每一线程都有一个ThreadLocalMap对象,对于每一个ThreadLocal对象来说它可以在每一个线程中ThreadLocalMap中存一个以它为key的值。例如:
- 在线程1中,ThreadLocal对象1执行set方法,实际上就是在线程1中的ThreadLocalMap中添加一个k-v(1号红色箭头),k就是ThreadLocal对象1。
- 在线程1中,ThreadLocal对象2执行get方法,就是在线程1的ThreadLocalMap中获取以ThreadLocal对象2为key的值(2号红色箭头)
每个ThreadLocal在某个线程中只能有一个对应value,因为Map中key不能重复,这就实现了线程唯一变量的功能。
下面看一下ThreadLocal中的get方法验证一下分析:
//ThreadLocal类中
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();
}
同时这也解释了,为什么上面代码中出现Exception in thread "pool-1-thread-1" java.lang.NullPointerException
,原因就是在新创建的线程中的ThreadLocalMap中没有以当前ThreadLocal对象为key的值。
3、java引用类型
3.1 GC回收对象
java垃圾回收器多采用可达性分析算法来确定一个java对象需不需要回收,过程是这样的:
- 首先确定一些一定不能回收的对象,叫做GCRoot对象
- 查找GCRoot对象引用的对象
- 然后沿着引用链一直找(注意:引用有多种类型)
最终根据对象被引用的类型、有没有被引用,来决定是不是回收这个对象。
3.2 引用类型
那么引用类型有哪些呢?总体而言分为四种:
- 强引用
沿着GC Root引用链可以找到的对象就是强引用对象 - 软引用 (SoftReference)
垃圾回收后仍内存不足,就会回收掉 - 弱号1用 (WeakReference)
垃圾回收就将其回收掉 - 虚引用 (PhantomReference)
必须配合引用队列使用,主要配合 ByteButfer 使用,被引用对象回收时,会将虛引用入队,由
Reference Handler 线程调用虚引用相关方法释放直接内存
4、内存泄漏问题
在看了java中几种引用类型之后在看内存泄漏问题,什么是内存泄漏?就是指一部分对象一直占用内存,也清不掉,就好像这块内存空间没了,漏掉了。
其实在ThreadLocalMap的内部使用的是Entry类存储k-v
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;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
//省略若干代码
}
可以看到Entry在实现时继承了弱引用,为什么这样呢?看注释是这样说的
The entries in this hash map extend WeakReference, using its main ref field as the key (which is always a ThreadLocal object). Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, so the entry can be expunged from table. Such entries are referred to as "stale entries" in the code that follows.
Note that null keys (i.e. entry.get()* == null)
如果 key threadlocal 为 null 了,这个 entry 就可以清除了。
ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收 。
在ThreadLocal的使用时,线程的ThreadLocalMap中都存储了以ThreadLocal为key的值,如果ThreadLocal被清理了,那么
ThreadLocalMap中对应的数据会被清理吗?
并不会,原因是ThreadLocal被清理变成null之后,ThreadLocalMap被Thread所引用(强引用)并不会回收,只是key变成了null。
如何解决呢内存泄漏呢?最简单的办法就是用完之后remove,ThreadLocal不用了,就去ThreadLocalMap中清理到对用的k-v。