天天看点

Spring事务(二、源码分析之ThreadLocal)ThreadLocal是什么ThreadLocal公共方法ThreadLocalMap缺点总结

Thread在管理request作用域的Bean、事务管理、任务调度、AOP等模块中都有它的身影,所以想了解Spring事务管理的底层技术,ThreadLocal是必须攻克的“山头堡垒”。

ThreadLocal是什么

ThreadLocal为解决多线程程序的并发问题提供了一种新的思路,使用这个工具类可以很简洁地编写出优美的多线程程序。

ThreadLocal,顾名思义,它不是一个线程,而且保存线程本地化对象的容器。当运行于多线程环境的某个对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以没和线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。

InheritableThreadLocal 继承于 ThreadLocal,它自动为子线程复制一份从父线程那里继承而来的本地变量:在创建子线程时,子线程会接收所有可继承的线程本地变量的初始值。当必须将本地线程变量自动传送给所有创建的子线程时,应尽可能地使用InheritableThreadLocal,而非ThreadLocal。

ThreadLocal公共方法

ThreadLocal主要是4个public方法,其他的方法都是辅助这4个方法。

get() - 获取线程局部变量的当前副本中的值

/**
     * 返回此线程局部变量的当前线程副本中的值。如果变量没有当前线程的值,
     * 则首先将其初始化为调用 initialvalue 方法返回的值。
     * @return 此线程的当前线程的本地值
     */
    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();
    }
           

set(T value) - 将此线程局部变量的当前线程副本设置为指定值

/**
     * 将此线程局部变量的当前线程副本设置为指定值。大多数子类将不需要重写这个方法,
     * 仅仅依靠 initialvalue 方法来设置线程局部变量的值。
     * @param value 要存储在此线程的当前线程本地副本中的值。
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
           

remove() - 删除此线程局部变量的当前线程值

/**
     * 删除此线程局部变量的当前线程值。如果此线程局部变量随后被当前线程@linkplain读取,
     * 则通过调用其 initialvalue 方法重新初始化其值,除非其值是 linkplain 由临时中的当前线程设置。
     * 这可能导致在当前线程中多次调用 initialvalue 方法。
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
           

withInitial(Supplier<? extends S> supplier) - 创建线程局部变量

/**
     * 创建线程局部变量。变量的初始值通过调用upplier上的get方法来确定。
     * @param<s>线程局部值的类型
     * @参数supplier用于确定初始值的供应商
     * @返回新的线程局部变量
     * @如果supplier为空,则引发NullPointerException
     * 
     * @since 1.8
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
           

我们可以看到,里面有一个内部数据结构ThreadLocalMap

ThreadLocalMap

虽然叫ThreadLocalMap,但是其并没有实现Map接口,其内部是自己实现的一个Entry对象,以及kye,value格式。

我们看下其重要方法:

Entry

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

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

注意

  1. Entry继承了WeakReference(弱引用)。
  2. 其key是写死的ThreadLocal类型。但Value可以为任意类型。
  3. Entry的kye是弱引用类型的,Value并非弱引用。

ThreadLocalMap其他参数

/**
         * 初始容量
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 表,大小必须是2的幂。
         */
        private Entry[] table;

        /**
         * 表大小
         */
        private int size = 0;

        /**
         * 要调整大小的下一个大小值。
         */
        private int threshold; // Default to 0

        /**
         * 阈值,设置最坏是长度的2/3。
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
           

作为一个map,肯定避免不了hash冲突以及扩容问题。那么ThreadLocalMap是如何实现的。

hash冲突

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

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

我们可以看到ThreadLocalMap使用的是最简单,步长加1或减1,寻找下一个相邻的位置。也是线性探测。

线性探测:

根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

上面介绍ThreadLocal 以及 ThreadLocalMap就暂时介绍完了。

我们下面说下ThreadLocalMap缺点:

ThreadLocalMap缺点

  1. 解决hash冲突效率低

因为是使用的是线性探测法,步长+1或者-1,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

  1. 弱引用内存泄露问题

    整理自https://www.jianshu.com/p/a1cd61fa22da

再说问题产生原因和解决办法前,我们先说下为什么要使用弱引用:

为什么要使用弱引用

从表面上看,发生内存泄漏,是因为Key使用了弱引用类型。但其实是因为整个Entry的key为null后,没有主动清除value导致。很多文章大多分析ThreadLocal使用了弱引用会导致内存泄漏,但为什么使用弱引用而不是强引用?

官方文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.

为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。

下面我们分两种情况讨论:

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。

产生泄露原因

ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,当ThreadLocal没有外部强引用来引用它的时候,ThreadLocal会在下次JVM垃圾收集时被回收。

这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

解决办法

但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

在get()方法中调用map.getEntry(this)时,其内部会判断key是否为null,继续看map.getEntry(this)源码:

getEntry(ThreadLocal<?> key)

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // 判断Entry是否为空,以及key是否为null
            if (e != null && e.get() == key)
                return e;
                // key为空调用getEntryAfterMiss()
            else
                return getEntryAfterMiss(key, i, e);
        }
           

getEntryAfterMiss(ThreadLocal<?> key, int i, Entry 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;
                    // 如果key == null,调用expungeStaleEntry
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

           

expungeStaleEntry(int staleSlot)

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot 
            // 设置value = null,删除value,便于下次回收。
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            // 循环检查,判断是否有key == null 的存在,如果有,一并将其value 设置为 null,方便回收
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
           

经过上面的步骤,其实也不能保证ThreadLocal不会发生内存泄漏,例如:

  • 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
  • 分配使用了ThreadLocal又不再调用get()、set()、remove()方法,那么就会导致内存泄漏。

总结

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

我们可能还听说过线程同步机制。它也是为了解决多线程中相同变量的访问冲突问题。那么二者相比有什么不同呢。

  • 线程同步机制:通过对象的锁机制保证同一时间只有一个线程访问变量。此时该变量是线程共享的,需要使用程序分析什么时候济宁读/写、什么时候锁定、什么时候释放等问题,程序升级和编写难度大。采用了“时间换空间”的方式。
  • ThreadLocal:为每个线程提供了一个独立的变量副本,从而隔离了多个线程对访问数据的冲突。因为每个线程都拥有自己的变量副本,所以没必要对该变量进行同步。可以把不安全的变量封装进ThreadLocal。采用了“空间换时间”的方式。