文章目录
- 为什么使用ThreadLocal
- ThreadLocal核心
- ThreadLocal内部结构
- ThreadLocal内存泄漏
- 解决内存泄漏
为什么使用ThreadLocal
在并发编程中,多个线程同时访问和修改共享变量是一个常见的场景。这种情况下,可能会出现线程安全问题,即多个线程对共享变量的操作可能会相互干扰,导致数据不一致。
为了解决线程安全问题,一种常见的做法是使用锁机制,如synchronized关键字或Lock接口。然而,加锁的方式可能会带来性能上的损失,因为线程之间需要竞争锁,而且在等待锁的过程中会阻塞线程的执行。
另一种解决方案是使用ThreadLocal。ThreadLocal提供了一种空间换时间的方式来解决线程安全问题。它为每个线程创建了一个独立的存储空间,用于保存线程特有的数据。当多个线程访问同一个ThreadLocal变量时,实际上它们访问的是各自线程本地存储的副本,而不是共享变量本身。因此,每个线程都可以独立地修改自己的副本,而不会影响到其他线程。
使用ThreadLocal的好处在于它避免了线程之间的竞争和阻塞,提高了并发性能。同时,它也简化了编程模型,因为开发者不需要显式地使用锁来保护共享变量的访问。
需要注意的是,ThreadLocal并不适用于所有场景。它主要适用于每个线程需要独立保存自己的数据副本的情况。如果多个线程之间需要共享数据并进行协作,那么使用锁或其他同步机制可能更为合适。此外,在使用ThreadLocal时也需要注意内存泄漏和数据污染的问题,需要正确地管理和清理线程本地存储的数据。
ThreadLocal核心
ThreadLocal是java包下的工具类,它提供了一种线程级别的数据隔离机制。通过ThreadLocal,我们可以在每个线程中存储自己的数据副本,互不影响,避免了多线程编程中的共享数据问题。
核心特点:
核心特性
1.线程隔离:每个线程对 ThreadLocal 变量的修改对其他线程是不可见的。
2.无继承性:子线程不能访问父线程的 ThreadLocal 变量,除非子线程中有显式的设置或复制操作。
3.避免同步:由于每个线程都有自己的变量副本,因此不需要同步就可以保证线程安全。
常见方法
public T get():返回当前线程对应的变量的值。如果当前线程没有对应的值,则返回初始值或 null(如果未设置初始值)。
public void set(T value):设置当前线程对应的变量的值。
public void remove():删除当前线程对应的变量。
protected T initialValue():这是一个受保护的方法,用于设置变量的初始值。通常,你可以通过匿名内部类来覆盖这个方法。
ThreadLocal内部结构
可以看到,ThreadLocal对象是存储在每个Thread线程内部的ThreadLocalMap中的,并且在ThreadLocalMap中有一个Entry数组,Entry数组中的每一个元素都是一个Entry对象。
每个Entry对象中存储着一个ThreadLocal对象与其对应的value值,每个Entry对象在Entry数组中的位置是通过ThreadLocal对象的threadLocalHashCode计算出来的,以此来快速定位Entry对象在Entry数组中的位置。所以,在Thread中,可以存储多个ThreadLocal对象。
ThreadLocal内存泄漏
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到,Entry类继承了WeakReference类,WeakReference类的泛型是ThreadLocal,,说明ThreadLocalMap中的Entry数组对Entry对象的Key就是弱引用。所以,Entry对象中的Key可以被GC自动回收。当Thread被回收后
当Entry对象中的Key被GC自动回收后,对应的ThreadLocal被GC回收掉了,变成了null,但是ThreadLocal对应的value值依然被Entry引用,不能被GC自动回收。
此时,我们可以看到,Entry对象中的Key,也就是ThreadLocal对象可以被GC自动回收,但是对应的value还在被引用,所以,value是不能被GC自动回收的,这种情况下就会存在内存泄露的风险。
解决内存泄漏
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
remove方法会将ThreadLocal为null对应的value设置为null,同时会把对应的Entry对象也设置为null,并且会将所有ThreadLocal对应的value为null的Entry对象设置为null,这样就去除了强引用,便于后续的GC进行自动垃圾回收,也就避免了内存泄露的问题。
所以要求我们在平时的代码中做到用完就remove