聊聊并发:(十八)ThreadLocal分析

前言

在前面的文章中,我们陆续对concurrent包中的常用类进行了依次介绍,涵盖范围包括各种锁、并发容器、队列,理解这些类的作用以及原理,可以帮助我们更好的应对并发场景下带来的挑战,如果您还对其中哪些类的实现不太熟悉,建议您阅读一下之前的文章。

本篇,我们来介绍一下ThreadLocal的作用及其原理,基于JDK1.8。

ThreadLocal介绍

ThreadLocal是线程内部的数据存储类,通过它可以指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,可以实现线程之间的数据访问隔离。

每个线程都保持对其线程局部变量副本的隐式引用,这里成立的前提条件是线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

ThreadLocal提供的方法比较简单,如下:

方法名称描述
T get()返回此线程局部变量的当前线程副本中的值。
protected T initialValue()返回此线程局部变量的当前线程的“初始值”。
void remove()移除此线程局部变量当前线程的值。
void set(T value)将此线程局部变量的当前线程副本中的值设置为指定值。

方法比较简单,我们可以为当前线程通过set()方法设定指定的值,通过get()方法来进行获取,在线程执行结束的时候,通过remove()方法销毁变量。

那么ThreadLocal是如何做到线程之间数据隔离的呢?我们来看一下具体实现。

ThreadLocal内部实现

ThreadLocal内部采用了一个Map的结构,来存储变量。

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

    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0

    /**
     * Set the resize threshold to maintain at worst a 2/3 load factor.
     */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

    /**
     * Increment i modulo len.
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    /**
     * Decrement i modulo len.
     */
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

    /**
     * Construct a new map initially containing (firstKey, firstValue).
     * ThreadLocalMaps are constructed lazily, so we only create
     * one when we have at least one entry to put in it.
     */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
    ....
}

其结构与HashMap相似,采用了数组的模型,存储元素。但是不同的是,存储元素的Entry对象继承了WeakReference,这意味着,其引用为null后,会被垃圾回收器进行回收。

最开始我们提到了ThreadLocal的作用,它为每一个线程保存了一份数据副本,而实现的方式是在Thread中提供了一个ThreadLocalMap的引用。

ThreadLocal.ThreadLocalMap threadLocals = null;

image

接下来,我们来看一个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 getMap(Thread t) {
    return t.threadLocals;
}

set()方法中,主要进行了几部操作:

1、获取当前线程

2、获取当前线程保存的ThreadLocalMap对象

3、如果ThreadLocalMap不为空,将元素进行存储

4、如果ThreadLocalMap为空,初始化ThreadLocalMap

再看一下ThreadLocalMap是如何新增元素的:

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

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

看到这里,如果您对HashMap的源码有所了解的话,一定非常的熟悉,但是ThreadLocalMap解决数组位置上元素冲突的方式,与HashMap并不一致,没有采用拉链表方式,而是采用线性探测法(不断加 1)。

根据ThreadLocal中初始化好的 threadLocalHashCode和table的长度进行与计算,计算出该ThreadLocal对应的数组下标,从该位置开始,遍历数组中的Entry元素,寻找当前ThreadLocal的key是否已经存在,如果存在,替换旧值。

如果entry里对应的key为null的话,表明此entry为staled entry,就将其替换为当前的key和value,replaceStaleEntry()的作用是 检查数组中的其他元素是否存在已经失效的情况,在行了不少的垃圾清理动作以防止引用关系存在而导致的内存泄露。

如果不存在,新增一个元素,放入数组中,并会进行启发式的垃圾清理,cleanSomeSlots()用于清理无用的Entry。

这里并不对其实现就进行深究。

在1.8中,为了避免弱引用带来的内存泄露问题,做了大量的引用清理实现。

ThreadLocal get()

我们再来看一下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();
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

get()方法的实现比较简单,获取当前线程的ThreadLocalMap,获取map中当前threadlocal对象对应的entry,如果entry不为空,获取对应的value,如果为空,重新初始化value。

这里对于getEntry()的实现,我们简要分析一下。

1、根据ThreadLocal中的threadLocalHashCode值计算出该元素在数组中的位置

2、判断元素不为空,且元素存储的key值为要获取的key

3、如果条件2不成立,那么说明可能存在键冲突,则调用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法返回entry

getEntryAfterMiss()的流程:

1、首要前提,entry元素不为空,获取entry对应的key

2、判断key是否一致,如果是,返回entry

3、如果key为null,那么expungeStaleEntry()方法清除过期元素

4、其他情况则通过nextIndex方法获取下一个索引位置index

5、获取新index处的entry,再死循环2/3/4,直到定位到该key返回entry或者返回null

expungeStaleEntry()的作用是清除无用的元素,该方法对于ThreadLocal非常的重要,它可以清理掉无用的元素,避免出现内存泄露的情况发生。

ThreadLocal remove()

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

private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

remove()的实现较为简单,获取当前线程对应的ThreadLocalMap,移除指定ThreadLocal对应的entry。

同样的,在entry的引用清除后,调用expungeStaleEntry()方法,清除掉引用。

ThreadLocal initialValue与withInitial

ThreadLocal提供了一个protected的初始化方法initialValue,用于实现其的子类可以进行初始化的动作,在JDK 1.8中,新增了withInitial的方法,支持Lambda表达式,该方法与initialValue作用一样,都用于初始化动作。

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        return supplier.get();
    }
}
private ThreadLocal<Object> threadLocalWithInitial = ThreadLocal.withInitial(Object::new);

ThreadLocal内存泄露

上面我们介绍了ThreadLocal的内部实现,我们知道了ThreadLocal内部对key值的存储是基于WeakReference弱引用的,意味着只有引用为null的时候,才可以被GC回收。

当一个线程调用ThreadLocal的set方法设置变量时候,当前线程的ThreadLocalMap里面就会存放一个记录,这个记录的key为ThreadLocal的引用,value则为设置的值。

如果当前线程一直存在而没有调用ThreadLocal的remove方法,并且这时候其它地方还是有对ThreadLocal的引用,则当前线程的ThreadLocalMap变量里面会存在ThreadLocal变量的引用和value对象的引用是不会被释放的,这就会造成内存泄露的。

在JDK1.8中,ThreadLocal对于这里做出了一定的优化,在set()、get()与remove()的时候,都会对无用对象进行扫描并清理,但这并不是及时的,也不是每次都会触发执行的,因此味着还是有可能会出现内存溢出的情况。

所以在使用完毕后即使调用remove方法才是解决内存泄露的王道。

关于ThreadLocal内存泄露的文章,可以参考这篇博文,解释的较为详细:

使用ThreadLocal不当可能会导致内存泄露

(http://ifeve.com/使用threadlocal不当可能会导致内存泄露/)

结语

本篇,我们介绍了ThreadLocal的使用及其实现机制,ThreadLocal在多线程开发场景下,是非常常用的,但是如果使用不当,也会造成比较大的麻烦,因此,合理使用好ThreadLocal,并且了解其内部机制,是非常重要的。希望本篇文章,能对您有所帮助。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值