概述
线程本地变量。当使用 ThreadLocal 维护变量时, ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。
每个线程都有一个 ThreadLocalMap ( ThreadLocal 内部类),Map中元素的键为 ThreadLocal ,而值对应线程的变量副本。
set
ThreadLocal是如何实现线程隔离的?
主要是用到了Thread对象中的一个ThreadLocalMap类型的变量。其实就是用了Map的数据结构给当前线程缓存了, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程的threadLocal;
调用 threadLocal.set() -->调用 getMap(Thread) -->返回当前线程的ThreadLocalMap --> map.set(this, value) ,this是 threadLocal 本身。源码如下:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//给当前线程的ThreadLocalMap设置值
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//this是 threadLocal 本身
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
map.set(this, value);方法
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//这里用的是Hash冲突的开放定址法的线性探测
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);// new了一个Entry对象,key是ThreadLocal对象
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- 首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals
- 如果当前线程的threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象;
- 如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个对象, 并且添加到当前线程的threadLocals Map中,并返回
- 如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个对象并添加到ThreadLocalMap对象中并返回。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);//父类是WeakReference,也就是相当于new了一个弱引用(k)
//也就相当于 map中的key是弱引用的
value = v;
}
}
这里的key指向的ThreadLocal是弱引用,是为了防止ThreadLocal对象永远不会被回收。
因为,若key为强引用,当ThreadLocal不想用了,那么就令tl=null,但是此时key中还有一个强引用指向ThreadLocal,因此也就永远无法进行回收(除非ThreadLocalMap不用了),所以会有内存泄露;但如果key使用的是弱引用,只要GC,就会回收
但是还会有内存泄漏存在,ThreadLocal被回收,就导致key=null,此时map中也就无法访问到value,无法访问到的value也就无用了,也就是说,这个k-v对无用了,那么value也应该被回收,但实际上value可能没有被回收,因此依然存在内存泄露
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
弱引用:GC时,若没有强引用指向这个对象了,只剩下弱引用,就会直接进行回收。原因就在于GC时无关内存是否足够,弱引用会被直接回收。所以,只要tl=null了,那么GC时,key指向的ThreadLocal对象就会被回收
get
调用 get() -->调用 getMap(Thread) -->返回当前线程的 ThreadLocalMap --> map.getEntry(this) ,返回 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();
}
threadLocals 的类型 ThreadLocalMap 的键为 ThreadLocal 对象,因为每个线程中可有多个threadLocal 变量,如 longLocal 和 stringLocal 。
public class ThreadLocalDemo {
ThreadLocal<Long> longLocal = new ThreadLocal<>();
public void set() {
longLocal.set(Thread.currentThread().getId());
}
public Long get() {
return longLocal.get();
}
public static void main(String[] args) throws InterruptedException {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
threadLocalDemo.set();
System.out.println(threadLocalDemo.get());
Thread thread = new Thread(() -> {
threadLocalDemo.set();
System.out.println(threadLocalDemo.get());
}
);
thread.start();
thread.join();
System.out.println(threadLocalDemo.get());
}
}
ThreadLocal 并不是用来解决共享资源的多线程访问问题,因为每个线程中的资源只是副本,不会共享。因此 ThreadLocal 适合作为线程上下文变量,简化线程内传参。
ThreadLocal内存泄漏的原因?
每个线程都有⼀个 ThreadLocalMap 的内部属性,map的key是 ThreaLocal ,定义为弱引用,value是强引用类型。垃圾回收的时候会⾃动回收key,而value的回收取决于Thread对象的生命周期。一般会通过线程池的方式复用线程节省资源,这也就导致了线程对象的生命周期比较长,这样便一直存在一条强引用链的关系: Thread --> ThreadLocalMap --> Entry --> Value ,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。
解决⽅法:每次使⽤完 ThreadLocal 就调⽤它的 remove() ⽅法,手动将对应的键值对删除,从⽽避免内存泄漏。
实际上源码中 每次进行set和get操作都会尽量将key为null的对象清理了。当然了这样也需要remove(),因为有可能长时间不进行set get操作,那么同样会占用内存
ThreadLocal使用场景有哪些?
场景1
线程池中的线程对象共享数据:线程池中的线程对象是可以被多个任务共享的,如果线程对象中需要保存任务相关的数据,使用 ThreadLocal 可以保证线程安全。
当然,在使用线程池时,ThreadLocal 可能会导致线程重用时的数据残留,从而影响程序的正确性。因此,在使用线程池时,要确保在任务执行前后清理 ThreadLocal 的值,以避免线程重用时的数据残留。
场景2
Web 应用中的请求处理:在 Web 应用中,一个请求通常会被多个线程处理,每个线程需要访问自己的数据,使用 ThreadLocal 可以确保数据在每个线程中的独立性。