ThreadLocal 翻译成中文是线程本地变量的意思,也就是说它是线程中的公有变量,每个线程只能操作本人的公有变量,所以不会造成线程不平安的问题。

线程不平安是指,多个线程在同一时刻对同一个全局变量做写操作时(读操作不会波及线程不平安问题),如果执行的后果和咱们预期的后果不统一就称之为线程不平安,反之,则称为线程平安。

在 Java 语言中解决线程不平安的问题通常有两种伎俩

  1. 应用锁(应用 synchronized 或 Lock);
  2. 应用 ThreadLocal。

锁的实现计划是在多线程写入全局变量时,通过排队一个一个来写入全局变量,从而就能够防止线程不平安的问题了。比方当咱们应用线程不平安的 SimpleDateFormat 对工夫进行格式化时,如果应用锁来解决线程不平安的问题,实现的流程就是这样的:

从上述图片能够看出,通过加锁的形式尽管能够解决线程不平安的问题,但同时带来了新的问题,应用锁时线程须要排队执行,因而会带来肯定的性能开销。然而,如果应用的是 ThreadLocal 的形式,则是给每个线程创立一个 SimpleDateFormat 对象,这样就能够防止排队执行的问题了,它的实现流程如下图所示:

PS:创立 SimpleDateFormat 也会耗费肯定的工夫和空间,如果线程复用 SimpleDateFormat 的频率比拟高的状况下,应用 ThreadLocal 的劣势比拟大,反之则能够思考应用锁。

然而,在咱们应用 ThreadLocal 的过程中,很容易就会呈现内存溢出的问题,如上面的这个事例。

什么是内存溢出?

内存溢出(Out Of Memory,简称 OOM)是指无用对象(不再应用的对象)继续占有内存,或无用对象的内存得不到及时开释,从而造成的内存空间节约的行为就称之为内存泄露。

内存溢出代码演示

在开始演示 ThreadLocal 内存溢出的问题之前,咱们先应用“-Xmx50m”的参数来设置一下 Idea,它示意将程序运行的最大内存设置为 50m,如果程序的运行超过这个值就会呈现内存溢出的问题,设置办法如下:

设置后的最终成果这样的:

PS:因为我应用的 Idea 是社区版,所以可能和你的界面不一样,你只须要点击“Edit Configurations...”找到“VM options”选项,设置上“-Xmx50m”参数就能够了。

配置完 Idea 之后,接下来咱们来实现一下业务代码。在代码中咱们会创立一个大对象,这个对象中会有一个 10m 大的数组,而后咱们将这个大对象存储在 ThreadLocal 中,再应用线程池执行大于 5 次增加工作,因为设置了最大运行内存是 50m,所以现实的状况是执行 5 次增加操作之后,就会呈现内存溢出的问题,实现代码如下:

import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;public class ThreadLocalOOMExample {        /**     * 定义一个 10m 大的类     */    static class MyTask {        // 创立一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)        private byte[] bytes = new byte[10 * 1024 * 1024];    }        // 定义 ThreadLocal    private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();    // 主测试代码    public static void main(String[] args) throws InterruptedException {        // 创立线程池        ThreadPoolExecutor threadPoolExecutor =                new ThreadPoolExecutor(5, 5, 60,                        TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));        // 执行 10 次调用        for (int i = 0; i < 10; i++) {            // 执行工作            executeTask(threadPoolExecutor);            Thread.sleep(1000);        }    }    /**     * 线程池执行工作     * @param threadPoolExecutor 线程池     */    private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {        // 执行工作        threadPoolExecutor.execute(new Runnable() {            @Override            public void run() {                System.out.println("创建对象");                // 创建对象(10M)                MyTask myTask = new MyTask();                // 存储 ThreadLocal                taskThreadLocal.set(myTask);                // 将对象设置为 null,示意此对象不在应用了                myTask = null;            }        });    }}

以上程序的执行后果如下:

从上述图片可看出,当程序执行到第 5 次增加对象时就呈现内存溢出的问题了,这是因为设置了最大的运行内存是 50m,每次循环会占用 10m 的内存,加上程序启动会占用肯定的内存,因而在执行到第 5 次增加工作时,就会呈现内存溢出的问题。

起因剖析

内存溢出的问题和解决方案比较简单,重点在于“起因剖析”,咱们要通过内存溢出的问题搞清楚,为什么 ThreadLocal 会这样?是什么起因导致了内存溢出?

要搞清楚这个问题(内存溢出的问题),咱们须要从 ThreadLocal 源码动手,所以咱们首先关上 set 办法的源码(在示例中应用到了 set 办法),如下所示:

public void set(T value) {    // 失去以后线程    Thread t = Thread.currentThread();    // 依据线程获取到 ThreadMap 变量    ThreadLocalMap map = getMap(t);    if (map != null)        map.set(this, value); // 将内容存储到 map 中    else        createMap(t, value); // 创立 map 并将值存储到 map 中}

从上述代码咱们能够看出 Thread、ThreadLocalMap 和 set 办法之间的关系:每个线程 Thread 都领有一个数据存储容器 ThreadLocalMap,当执行 ThreadLocal.set 办法执行时,会将要存储的值放到 ThreadLocalMap 容器中,所以接下来咱们再看一下 ThreadLocalMap 的源码:

static class ThreadLocalMap {    // 理论存储数据的数组    private Entry[] table;    // 存数据的办法    private void set(ThreadLocal<?> key, Object value) {        Entry[] tab = table;        int len = tab.length;        int i = key.threadLocalHashCode & (len-1);        for (Entry e = tab[i];                e != null;                e = tab[i = nextIndex(i, len)]) {            ThreadLocal<?> k = e.get();            // 如果有对应的 key 间接更新 value 值            if (k == key) {                e.value = value;                return;            }            // 发现空位插入 value            if (k == null) {                replaceStaleEntry(key, value, i);                return;            }        }        // 新建一个 Entry 插入数组中        tab[i] = new Entry(key, value);        int sz = ++size;        // 判断是否须要进行扩容        if (!cleanSomeSlots(i, sz) && sz >= threshold)            rehash();    }    // ... 疏忽其余源码}

从上述源码咱们能够看出:ThreadMap 中有一个 Entry[] 数组用来存储所有的数据,而 Entry 是一个蕴含 key 和 value 的键值对,其中 key 为 ThreadLocal 自身,而 value 则是要存储在 ThreadLocal 中的值

依据下面的内容,咱们能够得出 ThreadLocal 相干对象的关系图,如下所示:

也就是说它们之间的援用关系是这样的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因而当咱们应用线程池来存储对象时,因为线程池有很长的生命周期,所以线程池会始终持有 value 值,那么垃圾回收器就无奈回收 value,所以就会导致内存始终被占用,从而导致内存溢出问题的产生

解决方案

ThreadLocal 内存溢出的解决方案很简略,咱们只须要在应用完 ThreadLocal 之后,执行 remove 办法就能够防止内存溢出问题的产生了,比方以下代码:

import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;public class App {    /**     * 定义一个 10m 大的类     */    static class MyTask {        // 创立一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)        private byte[] bytes = new byte[10 * 1024 * 1024];    }    // 定义 ThreadLocal    private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();    // 测试代码    public static void main(String[] args) throws InterruptedException {        // 创立线程池        ThreadPoolExecutor threadPoolExecutor =                new ThreadPoolExecutor(5, 5, 60,                        TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));        // 执行 n 次调用        for (int i = 0; i < 10; i++) {            // 执行工作            executeTask(threadPoolExecutor);            Thread.sleep(1000);        }    }    /**     * 线程池执行工作     * @param threadPoolExecutor 线程池     */    private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {        // 执行工作        threadPoolExecutor.execute(new Runnable() {            @Override            public void run() {                System.out.println("创建对象");                try {                    // 创建对象(10M)                    MyTask myTask = new MyTask();                    // 存储 ThreadLocal                    taskThreadLocal.set(myTask);                    // 其余业务代码...                } finally {                    // 开释内存                    taskThreadLocal.remove();                }            }        });    }}

以上程序的执行后果如下:

从上述后果能够看出咱们只须要在 finally 中执行 ThreadLocal 的 remove 办法之后就不会在呈现内存溢出的问题了。

remove的机密

那 remove 办法为什么会有这么大的魔力呢?咱们关上 remove 的源码看一下:

public void remove() {    ThreadLocalMap m = getMap(Thread.currentThread());    if (m != null)        m.remove(this);}

从上述源码中咱们能够看出,当调用了 remove 办法之后,会间接将 Thread 中的 ThreadLocalMap 对象移除掉,这样 Thread 就不再持有 ThreadLocalMap 对象了,所以即便 Thread 始终存活,也不会造成因为(ThreadLocalMap)内存占用而导致的内存溢出问题了。

总结

本篇咱们应用代码的形式演示了 ThreadLocal 内存溢出的问题,严格来讲内存溢出并不是 ThreadLocal 的问题,而是因为没有正确应用 ThreadLocal 所带来的问题。想要防止 ThreadLocal 内存溢出的问题,只须要在应用完 ThreadLocal 后调用 remove 办法即可。不过通过 ThreadLocal 内存溢出的问题,让咱们搞清楚了 ThreadLocal 的具体实现,不便咱们日后更好的应用 ThreadLocal,以及更好的应答面试。

关注公号「Java中文社群」查看更多有意思、涨常识的并发编程文章。