这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战
一、前言
为什么多线程并发会有安全问题?
多个线程并发访问同一个共享数据的时候,才会出现安全问题。 因为
Java内存模型,多个线程并发修改同一个数据的时候,可能会导致数据错乱,所以需要加并发同步机制。
那么这样的话,避免访问共享数据,不就没有这个问题了。
每个线程访问自己本地的变量,就跟别的线程没有冲突了。
ThreadLocal 解决线程安全问题:就给在每个线程拷贝对应变量副本。
举个栗子:
- 线程1 设置1,获取对应数据
- 线程2 设置2, 获取对应数据
public class Test {
private static ThreadLocal<Long> requestId = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = generateThread("线程1", 1);
Thread thread2 = generateThread("线程2", 2);
thread1.start();
thread2.start();
}
private static Thread generateThread(String name, long num) {
Thread thread = new Thread(){
public void run() {
requestId.set(num);
System.out.println(this.getName() + " : " + requestId.get());
}
};
thread.setName(name);
return thread;
}
}
输出结果如下:
线程1 : 1
线程2 : 2
(1)源码剖析:ThreadLocal 线程本地副本的实现原理
JDK 中有个 ThreadLocal 类,其内部有一个 ThreadLoaclMap 内部类。
对应源码如下:
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;
}
}
// 数组
private Entry[] table;
//...
}
解释下 Entry,可以理解为一个 Map,其键值对为:
key键:为当前的ThreadLocalvalue值:实际需要存储的变量
ThreadLocalMap 既然叫 Map,那么就和 HashMap 类似,但是具体实现会有一些不同:
HashMap在hash冲突的时候,采用的是拉链法,长度大于 8会转为红黑树。ThreadLocalMap在hash冲突的时候,采用线性探测法,发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。
Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系
查看 Thread 源码可以发现,有对应 ThreadLocalMap 引用。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
// ... ...
}
这三者关系如图:
ThreadLocal 就是 key:
// key 要提前声明
private static ThreadLocal<Long> requestId = new ThreadLocal<>();
Thread thread = new Thread(){
public void run() {
requestId.set(1L);
System.out.println(this.getName() + " : " + requestId.get());
}
};
有个比较好玩的点,ThreadLocalMap 里装着是 ThreadLocal; 但在类定义中 ThreadLocalMap 是 ThreadLocal 的静态内部类,这设计可以揣摩下。
(2)ThreadLocal 和 Synchronized 有什么关系?
ThreadLocal和synchronized它们两个都能解决线程安全问题,那么ThreadLocal和synchronized是什么关系呢?
不同点如下:
ThreadLocal是通过让每个线程独享自己的副本,避免了资源的竞争。synchronized主要用于临界资源的分配,在同一时刻限制最多只有一个线程能访问该资源。
相比于 ThreadLocal 而言,synchronized 的效率会更低一些,但是花费的内存也更少。在这种场景下,ThreadLocal 和 synchronized 虽然有不同的效果,不过都可以达到线程安全的目的。
二、ThreadLocal 内存泄漏问题
问题:为什么每次用完
ThreadLocal都要调用remove()?
内存泄漏指的是:当某一个对象不再有用的时候,占用的内存却不能被回收,这就叫作内存泄漏。
因为通常情况下,如果一个对象不再有用,那么垃圾回收器 GC,就应该把这部分内存给清理掉。
-
这样的话,被清理掉这部分内存后续重新分配到其他的地方去使用;
-
否则,如果对象没有用,但一直不能被回收,这样的垃圾对象如果积累的越来越多,则会导致可用的内存越来越少,最后发生内存不够用的
OOM错误。
(1)内存泄漏问题
显而意见,ThreadLocal 内存泄漏分为两个方面:
key泄漏value泄漏
先来回顾下 Thread 引用情况:
Thread -> ThreadLocalMap -> Entry -> <key, value>
1)Key 的泄漏
线程在访问了
ThreadLocal之后,都会在它的ThreadLocalMap里面的Entry中去维护该ThreadLocal变量与具体实例的映射。
key 是什么?
key就是ThreadLocal线程本地变量,需要提前声明。
再回顾下这个 demo:
private static ThreadLocal<Long> requestId = new ThreadLocal<>();
Thread thread = new Thread(){
public void run() {
requestId.set(1L);
System.out.println(this.getName() + " : " + requestId.get());
}
};
可能会在业务代码中执行了 ThreadLocal requestId = null 操作,想清理掉这个 ThreadLocal 实例。
但是假设在 ThreadLocalMap 的 Entry 中强引用了 ThreadLocal 实:
- 那么,虽然在业务代码中把
ThreadLocal实例置为了null,但是在Thread类中依然有这个引用链的存在。 GC在垃圾回收的时候会进行可达性分析,它会发现这个ThreadLocal对象依然是可达的,所以对于这个ThreadLocal对象不会进行垃圾回收,这样的话就造成了内存泄漏的情况。
所以 ThreadLocalMap 中的 Entry 继承了 WeakReference 弱引用,源代码如下所示:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到,这个 Entry 是 extends WeakReference。
弱引用的特点是:如果这个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,所以弱引用不会阻止 GC。
因此,这个弱引用的机制就避免了 ThreadLocal 的内存泄露问题。
2)Value 的泄漏
ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,但是这个 Entry 包含了一个对 value 的强引用。
源代码如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到,value = v 这行代码就代表了强引用的发生。
Tips: JVM 垃圾回收是根据可达性进行分析。
情况可分为两种情况:
-
线程终止:线程生命周期都结束了,其下的
ThreadLocalMap就没有被引用,那就会被GC回收。引用关系:
Thread->ThreadLocalMap->Entry-><ThreadLocal, Value> -
线程运行:线程运行在线程池中,可以被反复使用,长时间内不会被销毁。这时候就可能出现
value泄漏因为
Value不再使用,但 线程Thread依然存活着。可达性分析,这个value仍是可达的。
JDK 同样也考虑到了这个问题,在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。
但是假设 ThreadLocal 已经不被使用了,那么实际上 set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value 的内存泄漏。
(2)如何避免内存泄露
分析完这个问题之后,该如何解决呢?
解决方法就是:调用 ThreadLocal 的 remove 方法。
调用这个方法就可以删除对应的
value对象,可以避免内存泄漏。
remove 方法的源码:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
可以看出,它是先获取到 ThreadLocalMap 这个引用的,并且调用了它的 remove 方法。这里的 remove 方法可以把 key 所对应的 value 给清理掉,这样一来,value 就可以被 GC 回收了。
所以,在使用完了 ThreadLocal 之后,应该手动去调用它的 remove 方法,目的是防止内存泄漏的发生。