ThreadLocal和FastThreadLocal对比

1.ThreadLocal

1.1 TheadLocal介绍和用途

ThreadLocal是一个可以提供线程局部变量的一个类,每个线程包含变量的副本,所有线程的副本的修改都是互相独立的,互不影响,通常使用场景是用户登录态相关的内容。

1.2 原理介绍

首先我们看下数据结构简介图:

img

Thread类中包含ThreadLocalMap对象,ThreadLocalMap是ThreadLocal的静态内部类,ThreadLocalMap中的对象都是以ThreadLocal对象作为key存储对应的value。

从上面的结构图,我们已经窥见ThreadLocal的核心机制:

  • 每个Thread线程内部都有一个ThreadLocalMap。
  • Map里面存储线程本地对象(key)和线程的变量副本(value)
  • 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

这里我们以ThreadLocal的set方法来分析其原理,get方法与其类似:

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方法首先获取当前线程,然后获取当前线程的ThreadLocalMap对象,接着以ThreadLocal对象为key,传入的value值作为Map的value进行存储。因为每个线程都拥有自己的ThreadLocalMap对象,所以ThreadLocal的set方法设置的值并不影响其他线程,简单来说就是ThreadLocal对象是线程的本地变量。

1.3 内存泄漏问题

1.3.1 弱引用

/**
 * The entries in this hash map extend WeakReference, using
 * its main ref field as the key (which is always a
 * ThreadLocal object).  Note that null keys (i.e. entry.get()
 * == null) mean that the key is no longer referenced, so the
 * entry can be expunged from table.  Such entries are referred to
 * as "stale entries" in the code that follows.
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

从源码里面我们可以看到ThreadLocal的key使用的是弱引用,这里使用弱引用是为了防止内存泄漏。下图是ThreadLocal的整个引用链关系,key使用弱引用可以保证ThreadLocal对象在不再使用的情况下可以被正常回收,如果ThreadLocal对象没有使用,则说明其他对象没有引用此对象,此时只有ThreadLocalMap的Entry对象对其有引用,因为是弱引用,所以在GC的时候此对象会被回收,如果是强引用,则ThreadLocal对象将无法被回收。

1.3.2 正确调用remove方法

即便是ThreadLocalMap的key使用的是弱引用,可以被正常清理,但是ThreadLocal对象不再使用时,value。仍然无法被清理,仍然存在内存泄漏的问题。这时会有人问,那直接将value变成弱引用不就可以了吗,这样的话会导致value被错误回收,因为value除了被Entry引用之外没有其他的对象引用,因此会造成误清理,而key本身就是ThreadLocal对象,所以不会存在这个问题

那么我们如果解决内存泄漏的问题吗,这里Java已经给我们考虑好,提供了remove方法将Entry对象清理掉,在我们使用完ThreadLocal对象后调用这个方法可以解决这个问题,使用线程池的时候特别需要关注,因为线程是公用的,所以如果不清理的话会造成ThreadLocal变量线程不安全。

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) {
            // 这实际调用的是Reference类的clear方法
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}
/**
 * Clears this reference object.  Invoking this method will not cause this
 * object to be enqueued.
 *
 * <p> This method is invoked only by Java code; when the garbage collector
 * clears references it does so directly, without invoking this method.
 */
public void clear() {
    this.referent = null;
}

从源码中我们可以发现调用clear方法后,entry对象整个都会被回收。

2.FastThreadLocal

1.1 FastThreadLocal介绍

整个是netty框架的一个类,主要配合FastThreadLocalThread使用,保存线程的本地变量,作用和ThreadLocal类似,其性能更高,而且不需要关系内存泄漏的问题

1.2 原理介绍

1.2.1 get方法

下面我们来看下这个的原理,我们将探讨为什么它比Java自带的ThreadLocal的性能更高

img

上图是FastThreadLocal的大概的数据结构,我们发现实际存储value的结构变成了一个数组,其查询的时间变成o(1),而ThreadLocal的是map,其查询时间为o(m),因此在数据量大的情况下,FastThreadLocal的性能更高,但是其所需的空间更大,典型的***拿空间换时间***。下面我们以其get方法为例来说明其原理。

从下面的代码可以看出这个get方法包含三个步骤:

  1. 获取当前线程的InternalThreadLocalMap
  2. 从Map中获取value
  3. 如果value不是默认值(new Object())),则直接返回对象
  4. 否则返回初始化对象
/**
 * Returns the current value for the current thread
 */
public final V get() {
    // 1.获取当前线程的InternalThreadLocalMap
    InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
    // 2.根据index从Map中获取value
    Object v = threadLocalMap.indexedVariable(index);
    // 3.如果value不是默认值(new Object())),则直接返回对象
    if (v != InternalThreadLocalMap.UNSET) {
        return (V) v;
    }
    // 4.否则返回初始化对象
    return initialize(threadLocalMap);
}

首先我们开看下get方法步骤1:获取当前线程的InternalThreadLocalMap,

public static InternalThreadLocalMap get() {
    // 1.获取当前线程
    Thread thread = Thread.currentThread();
    // 2.判断当前线程是否FastThreadLocalThread,如果是则调用fastGet方法
    if (thread instanceof FastThreadLocalThread) {
        return fastGet((FastThreadLocalThread) thread);
    } else {
        // 3.否则调用slowGet方法
        return slowGet();
    }
}
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
    InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
    if (threadLocalMap == null) {
        thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
    }
    return threadLocalMap;
}
private static InternalThreadLocalMap slowGet() {
    // 1.获取InternalThreadLocalMap类型的ThreadLocal对象,这里的UnpaddedInternalThreadLocalMap是InternalThreadLocalMap
    // 的父类,在执行父类的构造方法的时候会创建一个初始化slowThreadLocalMap对象
    ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = UnpaddedInternalThreadLocalMap.slowThreadLocalMap;
    // 2.获取ThreadLocal对象中的InternalThreadLocalMap对象
    InternalThreadLocalMap ret = slowThreadLocalMap.get();
    // 如果为null,则直接创建新的InternalThreadLocalMap对象,并给ThreadLocal设置value值
    if (ret == null) {
        ret = new InternalThreadLocalMap();
        slowThreadLocalMap.set(ret);
    }
    return ret;
}

这是InternalThreadLocalMap的一个方法,这个方法其实适配了Java的Thread线程,如果在非FastThreadLocalThread线程中创建FastThreadLocal对象,则也可以使用。主要包含三个步骤:

  • 获取当前线程
  • 判断当前线程是否FastThreadLocalThread,如果是则调用fastGet方法,实际上我们看到fastGet方法就是返回当前线程的InternalThreadLocalMap对象
  • 否则调用slowGet方法,这个用到ThreadLocal对象进行曲线救国,返回当前线程的InternalThreadLocalMap对象

接着我们看下get方法的步骤2:从Map中获取value,

public Object indexedVariable(int index) {
    // 1.获取所有的变量的数组
    Object[] lookup = indexedVariables;
    // 根据下标从数据中获取数据
    return index < lookup.length? lookup[index] : UNSET;
}

这个方法是InternalThreadLocalMap中的一个方法,我最开始在看这个方法的时候感觉有点蒙蔽,这个index是如何计算的呢?我们可以发现index是从InternalThreadLocal类中传入的,并且传入的是其属性,那么我们接下来看下这个index是什么时候被赋值的。

private final int index;
public FastThreadLocal() {
    index = InternalThreadLocalMap.nextVariableIndex();
}

从下面的代码中我们可以发现index是一个final变量,是在构造函数中被初始化的,调用的是InternalThreadLocal的nextVariableIndex方法。

static final AtomicInteger nextIndex = new AtomicInteger();
public static int nextVariableIndex() {
    int index = nextIndex.getAndIncrement();
    if (index < 0) {
        nextIndex.decrementAndGet();
        throw new IllegalStateException("too many thread-local indexed variables");
    }
    return index;
}

我们可以发现每次调用nextVariableIndex方法时nextIndex都会自增,这样我们就明白其原理了,每次我们创建一个InternalThreadLocalMap对象时InternalThreadLocal都会为其按照顺序分配一个index,这个index对于每个InternalThreadLocal都是不变的,get时直接根据下标从数组中获取数据,set时根据下标对数组设置数据。

这里其实有个比较有意思的东西,实际上对于FastThreadLocalThread来说nextIndex完全没有必要设置为AtomicInteger,这里这样设置是为了保证在普通线程里面使用FastThreadLocal时保证线程安全,对于FastThreadLocalThread来说始终都是一个线程,但是对于普通的Thread来说,可能在当前线程里面使用多线程进行多个FastThreadLocal的创建,这样就会出现线程安全问题。

好了,扛不住了,明天继续分析后续的代码吧,睡觉。

1.2.2 set方法

set方法的大部分原理和get方法类似,但是也有些许不同,这里主要讲解set方法的一些特殊处理。

/**
 * Set the value for the current thread.
 */
public final void set(V value) {
    if (value != InternalThreadLocalMap.UNSET) {
        // 这个方法和set类似,这里不再说明
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        setKnownNotUnset(threadLocalMap, value);
    } else {
        // 如果放入的是空对象,则直接清理掉
        remove();
    }
}

我们发现开始的获取InternalThreadLocalMap方法和get一样,这里主要说明setKnownNotUnset方法,remove方法会在后面的removeAll方法进行详细讲解。

/**
 * @return see {@link InternalThreadLocalMap#setIndexedVariable(int, Object)}.
 */
private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
    // 判断是否当前ftl对象需要添加到set集合中,并设置设置当前value值
    if (threadLocalMap.setIndexedVariable(index, value)) {
        // 如果需要,则将其添加到需要清理的set集合中
        addToVariablesToRemove(threadLocalMap, this);
    }
}

/**
 * @return {@code true} if and only if a new thread-local variable has been created
 * 当且仅当ftl对象是新创建的时候返回true,这里可以理解为会将每次创建的ftl对象添加到清理的set集合中,这个集合永远处于fslm中的0槽位
 * 通过这个set,我们可以快速清理整个ftl对象,而不需要遍历fslm的数组
 */
public boolean setIndexedVariable(int index, Object value) {
    Object[] lookup = indexedVariables;
    if (index < lookup.length) {
        Object oldValue = lookup[index];
        lookup[index] = value;
        return oldValue == UNSET;
    } else {
        expandIndexedVariableTableAndSet(index, value);
        return true;
    }
}

private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
    // 从ftlm中获取需要清理的ftl类型的set对象
    // private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex(); 实际上variablesToRemoveIndex是ftlm的一个槽位,用来专门存储需要清理的ftl
    Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
    Set<FastThreadLocal<?>> variablesToRemove;
    // 如果此对象为空,则重新创建一个set集合用来存放需要清理的ftl对象
    if (v == InternalThreadLocalMap.UNSET || v == null) {
        variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
        threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
    } else {
        // 否则进行强转
        variablesToRemove = (Set<FastThreadLocal<?>>) v;
    }
	// 将需要清理的对象添加到set集合中
    variablesToRemove.add(variable);
}

我们发现setKnownNotUnset很简单,只有两个步骤:

  • 判断是否当前ftl对象需要添加到set集合中,并设置设置当前value值
  • 如果需要,则将其添加到需要清理的set集合中

我们首先看下步骤1——setIndexedVariable方法:

这个方法主要分为两种情况:

情况一:不需要扩容,则直接将value放入Object数组中即可,然后根据当前index原来的值是否是UNSET来判断此次操作是否是新增

情况二:需要扩容,则进行扩容,每次扩容都是2倍扩容,这个和HashMap的扩容方法类似,然后返回true(需要扩容表明一定是新增)

所以此方法的返回值也是耐人寻味的:当且仅当ftl对象是新创建的时候返回true,这里为什么需要这个操作,刚开始的时候一直很懵逼,后来突然想明白了,其实很简单,ftlm想将所有的ftl对象保存到一起,这样在清理ftl对象的时候可以快速清理掉所有对象,判断是新建的原因为不想多次添加重复值,提高效率。

最后我们来看下addToVariablesToRemove方法,将ftl对象添加到需要清理的对象里面,主要分为以下步骤:

  • 从ftlm中获取需要清理的ftl类型的set对象
  • private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex(); 实际上variablesToRemoveIndex是ftlm的一个槽位,用来专门存储需要清理的ftl,这里我们可以看到一个变量variablesToRemoveIndex,我们发现是一个static类型的常量,这个变量最终会初始化为0(InternalThreadLocalMap.nextVariableIndex()方法在get方法里面有讲解,这里不再赘述),所以实际上我们可以确定保存所有ftl对象的set集合实际上是***放在ftlm的0槽位***的,这个是永远不会变化的。
  • 如果此对象为空,则重新创建一个set集合用来存放需要清理的ftl对象
  • 将需要清理的对象添加到set集合中

1.2.3 removeAll方法

我们知道,jdk自带的ThreadLocal对象存在内存泄漏的问题,需要手动进行清理,而ftl则不需要,下面我们来看下为什么ftl不需要手动清理。

final class FastThreadLocalRunnable implements Runnable {
    private final Runnable runnable;

    private FastThreadLocalRunnable(Runnable runnable) {
        this.runnable = ObjectUtil.checkNotNull(runnable, "runnable");
    }

    @Override
    public void run() {
        try {
            runnable.run();
        } finally {
            FastThreadLocal.removeAll();
        }
    }

    static Runnable wrap(Runnable runnable) {
        return runnable instanceof FastThreadLocalRunnable ? runnable : new FastThreadLocalRunnable(runnable);
    }
}

上面的代码是netty的FastThreadLocalRunnable,netty的线程池创建的就是这种线程,我们发现其run方法在finally块中统一执行了FastThreadLocal.removeAll()方法,这样在每次线程执行结束的时候都会清理掉ftl对象,不会造成内存溢出的问题。因为ftl也可以用于普通线程,所以在普通线程中使用的时候我们记得一定要执行removeAll方法,否则会造成内存溢出问题。

/**
 * Removes all {@link FastThreadLocal} variables bound to the current thread.  This operation is useful when you
 * are in a container environment, and you don't want to leave the thread local variables in the threads you do not
 * manage.
 */
public static void removeAll() {
    // 获取当前线程的ftlm对象
    InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();
    if (threadLocalMap == null) {
        return;
    }

    try {
        // 从ftlm中获取保存所有ftl对象的set
        Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
        if (v != null && v != InternalThreadLocalMap.UNSET) {
            @SuppressWarnings("unchecked")
            Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
            FastThreadLocal<?>[] variablesToRemoveArray =
                    variablesToRemove.toArray(new FastThreadLocal[0]);
            // 遍历set集合,执行remove方法清理ftl对象
            for (FastThreadLocal<?> tlv: variablesToRemoveArray) {
                tlv.remove(threadLocalMap);
            }
        }
    } finally {
        // 最后将ftlm清理掉
        InternalThreadLocalMap.remove();
    }
}

public static InternalThreadLocalMap getIfSet() {
    Thread thread = Thread.currentThread();
    if (thread instanceof FastThreadLocalThread) {
        return ((FastThreadLocalThread) thread).threadLocalMap();
    }
    return slowThreadLocalMap.get();
}

下面我们来分析removeAll方法:

  • 获取当前线程的ftlm对象,getIfSet这个方法和前面的InternalThreadLocalMap.get()方法一样,这里不再赘述
  • 从ftlm中获取保存所有ftl对象的set
  • 遍历set集合,执行remove方法清理ftl对象
  • 最后将ftlm清理掉

我们先来看下ftl的remove方法:

/**
 * Sets the value to uninitialized for the specified thread local map;
 * a proceeding call to get() will trigger a call to initialValue().
 * The specified thread local map must be for the current thread.
 */
@SuppressWarnings("unchecked")
public final void remove(InternalThreadLocalMap threadLocalMap) {
    if (threadLocalMap == null) {
        return;
    }
	// 获取当前ftl对象的value值,并从ftlm的Object数据中将此对象删除
    Object v = threadLocalMap.removeIndexedVariable(index);
    removeFromVariablesToRemove(threadLocalMap, this);
	// 如果当前value不是空对象,则执行ftl的onRemoval回调方法
    if (v != InternalThreadLocalMap.UNSET) {
        try {
            onRemoval((V) v);
        } catch (Exception e) {
            PlatformDependent.throwException(e);
        }
    }
}

主要包含以下步骤:

  • 获取当前ftl对象的value值,并从ftlm的Object数据中将此对象删除
  • 如果当前value不是空对象,则执行ftl的onRemoval回调方法

我们看下removeFromVariablesToRemove方法,我们发现这个方法主要就是从set集合删除ftl对象。

private static void removeFromVariablesToRemove(
        InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {

    Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);

    if (v == InternalThreadLocalMap.UNSET || v == null) {
        return;
    }

    @SuppressWarnings("unchecked")
    Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
    variablesToRemove.remove(variable);
}

最后是执行onRemoval回调方法,这个方法在ftl中是protected方法,由使用者重写此方法进行回调通知。

好了,以上就是整个ThreadLocal和FastThreadLocal的一个对比,花了挺长时间的,这个是第一个自己读源码并手写的笔记,花了大概一周的时间,有点惭愧,希望这是一个好的开头吧。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值