作者:半身风雪
上篇:Java 天生就是多线程
系列文章简介:上一节我们都明白了为什么Java 天生就是多线程,这一节我们一起来学习ThreadLocal进阶解析。
ThreadLocal 和 Synchonized 都用于解决多线程并发訪问。可是 ThreadLocal 与 synchronized 有本质的差别。synchronized 是利用锁的机制,使变量或代码块 在某一时该仅仅能被一个线程访问。而 ThreadLocal 为每个线程都提供了变量的 副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线 程对数据的数据共享。
ThreadLocal 类接口很简单,只有 4 个方法,我们先来了解一下:
void set(Object value)
public Object get()
public void remove()
protected Object initialValue()
public final static ThreadLocal RESOURCE = new ThreadLocal();
RESOURCE代表一个能够存放String类型的ThreadLocal对象。 此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是 线程安全的。
先来看一下源码:
上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap, ThreadLocalMap 是 ThreadLocal 的静态内部类,然后 Thread 类中有一个这样类型 成员,所以 getMap 是直接返回 Thread 的成员。
看下 ThreadLocal 的内部类 ThreadLocalMap 源码:
可以看到有个 Entry 内部静态类,它继承了 WeakReference,总之它记录了 两个信息,一个是 ThreadLocal
类型,一个是 Object 类型的值。getEntry 方法 则是获取某个 ThreadLocal 对应的值,set 方法就是更新或赋值相应的 ThreadLocal 对应的值。
回顾我们的 get 方法,其实就是拿到每个线程独有的 ThreadLocalMap
然后再用 ThreadLocal 的当前实例,拿到 Map 中的相应的 Entry,然后就可 以拿到相应的值返回出去。当然,如果 Map 为空,还会先进行 map 的创建,初始化等工作。
上图中的 o,我们可以称之为对象引用,而 new Object()我们可以称之为在内存 中产生了一个对象实例。
当写下 o=null 时,只是表示 o 不再指向堆中 object 的对象实例,不代表这 个对象实例不存在了。
强引用就是指在程序代码之中普遍存在的,类似“Object obj=new Object()
” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。
软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象, 在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行 第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时, 无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在 JDK 1.2 之 后,提供了 WeakReference 类来实现弱引用。
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用。
下面我们来写一段代码,展示内存泄漏的现象:
public class ThreadLocalOOM {
private static final int TASK_LOOP_SIZE = 500;
// 这里创建了5个线程池,大小固定为5 个线程,不明白没关系,关注我,后期会讲解
final static ThreadPoolExecutor poolExecutor = new
ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES, new LinkedBlockingDeque<>());
static class LocalVariable {
// 5M 大小的意思
private byte[] a = new byte[1024 * 1024 * 5];
}
final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < TASK_LOOP_SIZE; i++) {
poolExecutor.execute(new Runnable() {
@Override
public void run() {
new LocalVariable();
System.out.println("use local variable");
}
});
Thread.sleep(100);
}
}
}
运行上面代码,并将堆内存大小设 置为-Xmx256m。
可以看到内存的实际使用控制在 25M 左右:因为每个任务中会不断 new 出 一个 5M 的数组,5*5=25M,这是很合理的。
当我们启用了 ThreadLocal 以后:
内存占用最高升至 150M,一般情况下稳定在 90M 左右,那么加入一个 ThreadLocal 后,内存的占用真的会这么多?
于是,我们加入一行代码:
再执行,看看内存情况:
可以看见最高峰的内存占用也在 25M 左右,完全和我们不加 ThreadLocal 表 现一样。这就充分说明,确实发生了内存泄漏。
根据我们前面对 ThreadLocal 的分析,我们可以知道每个 Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需 要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。仔细观察 ThreadLocalMap,这个 map 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
因此使用了 ThreadLocal 后,引用链如图所示:
图中的虚线表示弱引用。
下面我们分两种情况讨论:
比较两种情况,我们可以发现:
由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障。
因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引 用。
JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。 JVM 利用调用 remove、get、set方法的时候,回收弱引用。
当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。
使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的 重复运行的,从而也就造成了 value 可能造成累积的情况。