暴力突破 Java 并发 - ThreadLocal 原理解析

一、ThreadLocal 使用


ThreadLocal 是一个关于创建线程局部变量的类。通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用 ThreadLocal 建的变量只能被当前线程访问,其他线程则无法访问和修改。接下来我们 ThreadLocal 如何使用:

创建,支持泛型:

ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>();

set方法:

mStringThreadLocal.set("name");

get方法:

mStringThreadLocal.get();

完整的使用示例:

private void testThreadLocal() {
    Thread t = new Thread() {
        ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>();

        @Override
        public void run() {
            super.run();
            mStringThreadLocal.set("name");
            mStringThreadLocal.get();
        }
    };

    t.start();
}

为 ThreadLocal 设置默认的 get 初始值,需要重写 initialValue 方法,下面是一段代码,我们将默认值修改成了线程的名字:

ThreadLocal<String> mThreadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
      return Thread.currentThread().getName();
    }
};

在 Android 中的应用,Looper 类就是利用了 ThreadLocal 的特性,保证每个线程只存在一个 Looper 对象:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

 

二、ThreadLocal 原理


为了更好的掌握 ThreadLocal,了解其内部实现是很有必要的,这里我们以 set 方法起始看一看 ThreadLocal 的实现原理。下面是 ThreadLocal 的 set 方法:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

大致意思为首先获取当前线程,利用当前线程作为句柄获取一个 ThreadLocalMap 的对象,如果上述 ThreadLocalMap 对象不为空,则设置值,否则创建这个 ThreadLocalMap 对象并设置值。

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

上面是一个利用 Thread 对象作为句柄获取 ThreadLocalMap 对象的代码。它获取的实际上是 Thread 对象的 threadLocals 变量,可参考下面代码:

class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

而如果一开始设置,即 ThreadLocalMap 对象未创建,则新建 ThreadLocalMap 对象,并设置初始值。

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

实际上 ThreadLocal 的值是放入了当前线程的一个 ThreadLocalMap 实例中,所以只能在本线程中访问,其他线程无法访问。

ThreadLocalMap 是什么呢?从名字上看,可以猜到它也是一个类似HashMap的数据结构,但是在ThreadLocal中,并没实现Map接口。在 ThreadLoalMap 中,也是初始化一个大小 16 的 Entry 数组,Entry 对象用来保存每一个 key-value 键值对,只不过这里的 key 永远都是 ThreadLocal 对象,是不是很神奇,通过 ThreadLocal 对象的 set 方法,结果把 ThreadLocal 对象自己当做 key,放进了 ThreadLoalMap 中。value 则是要存储的对象。这里需要注意的是,ThreadLoalMap 的 Entry 是继承 WeakReference,和 HashMap 很大的区别是,Entry 中没有 next 字段,所以就不存在链表的情况了。

 有了上面的基础,我们看 get() 方法就一点都不难理解了:

    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();
    }

对象存放在哪里呢?

在 Java 中,栈内存归属于线程私有,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。那么是不是说 ThreadLocal 的实例以及其值存放在栈上呢?其实不是,因为 ThreadLocal 实例实际上也是被其创建的类持有(更顶端应该是被线程持有)。而 ThreadLocal 的值其实也是被线程实例持有。它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

 

三、会导致内存泄露么


为什么 ThreadLocal 可能导致内存泄漏?先看看 Entry 的实现:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

通过之前的分析已经知道,当使用 ThreadLocal 保存一个 value 时,会在 ThreadLocalMap 中的数组插入一个Entry对象,按理说 key-value 都应该以强引用保存在 Entry 对象中,但在 ThreadLocalMap 的实现中,key 被保存到了 WeakReference 对象中。这就导致了一个问题,ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,如果创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,发生内存泄露。

既然已经发现有内存泄露的隐患,自然有应对的策略,在调用 ThreadLocal 的 get()、set() 可能会清除 ThreadLocalMap 中 key 为 null 的 Entry 对象,这样对应的 value 就没有 GC Roots 可达了,下次 GC 的时候就可以被回收,当然如果调用 remove 方法,肯定会删除对应的 Entry 对象。如果使用 ThreadLocal 的 set 方法之后,没有显示的调用 remove 方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完 ThreadLocal 之后,记得调用 remove 方法。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("android");
    // 其它业务逻辑
} finally {
    localName.remove();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值