likes
comments
collection
share

ThreadLocal设计思想浅析通过阅读这篇文章,你将可以学习到: ThreadLocal的使用方法与典型应用场景 T

作者站长头像
站长
· 阅读数 54

如果你发现你的“此时此地”变得无法忍受并且使你非常不开心,这时你有三种选择:从这种状况中离开,改变它,或者完全接受它。

ThreadLocal设计思想浅析通过阅读这篇文章,你将可以学习到: ThreadLocal的使用方法与典型应用场景 T

通过阅读这篇文章,你将可以学习到:

  1. ThreadLocal的使用方法与典型应用场景
  2. ThreadLocal的核心概念——ThreadLocalMap
  3. ThreadLocal的设计思路——映射关系维护、生命周期管理
  4. ThreadLocal使用过程中的那些大坑

接下来进入这些知识的分析和讲解过程。

ThreadLocal的使用方法与典型应用场景

ThreadLocaljava.lang包里面提供的类,正如其名字所示,它可以提供线程独占的变量,每一个线程,都可以拥有一份只有它自己使用的ThreadLocal对象集合。不同线程之间,即使是同名的对象,也不是同一个实例。

该对象,如果不是手动释放,它的生命周期会始终持续到线程运行结束。

使用方法

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
        new ThreadLocal<Integer>() {
            Override protected Integer initialValue() {
                return nextId.getAndIncrement();
        }
    };

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }
}

一个典型的用例如上,使用泛型声明一个静态的ThreadLocal变量,通过覆写initialValue函数,为这个变量赋值,同时,AtomicInteger保证了不同线程之间,生成变量值的唯一性。这样,线程内部通过调用ThreadId.get(),可以获取到该线程唯一的id

可以思考下,如果不借助ThreadLocal,要如何实现上述“为每个Thread赋值唯一的threadId的需求——在Thread类里面增加成员变量threadId,并且在Thread的构造函数里,由一个全局Factory为该threadId赋值(同样是借助AtomicInteger)。不仅实现方案要繁琐许多,而且一旦这个变量不是Integer类型,而是String或者Object呢?势必需要大量全局Factory

典型应用场景

ThreadLocal的应用场景有3个最主要的特征:

  • 并发环境,多个线程需要访问同一个对象
  • 该对象在被调用时,内部状态会发生不可预测的变化
  • 该对象创建成本高

典型的就是SimpleDateFormat,我们知道它一般用于格式化日期字符串,将Date对象转化为String

new SimpleDateFormat("yyyyMMdd HHmm").format(date)

SimpleDateFormat对象并非线程安全的,这里截取它代码中的片段,不安全处已经用注释标明:

    // Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date); // 注意!这里calendar是成员变量,在并发调用情况下,旧值会被新值覆盖

        boolean useDateFormatSymbols = useDateFormatSymbols();

        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
                count = compiledPattern[i++] << 16;
                count |= compiledPattern[i++];
            }

            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char)count);
                break;

            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;

            default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
            }
        }
        return toAppendTo;
    }

在并发环境下,我们当然可以为每一个线程单独创建一个SimpleDateFormat对象,用于格式化日期。但要知道,SimpleDateFormat对象创建成本是很高的,这显然会对程序运行效率造成负面影响。另一个手段是,在访问该变量的函数上加锁,通过临界区来保证同一时间下只有一条线程使用该对象。方案二同样不利于程序运行效率,rejected!

这时便可以使用ThreadLocal,即降低了重复创建对象造成的内存消耗,又能避免并发调用导致的状态问题。

public class Foo
{
    // SimpleDateFormat is not thread-safe, so give one to each thread
    private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue()
        {
            return new SimpleDateFormat("yyyyMMdd HHmm");
        }
    };

    public String formatIt(Date date)
    {
        return formatter.get().format(date);
    }
}

ThreadLocal的核心概念

ThreadLocal本质上是通过类似HashMap的键值对结构,来维护Thread独占的实例集合。

核心概念之ThreadLocalMap

ThreadLocalMapThreadLocal的静态内部类,由于是静态,不持有外部对象引用,以下是源码,次要代码已略去。

    static class ThreadLocalMap {
        // 继承自WeakReference,将传入的ThreadLocal对象包装为key,value则是对应泛型的值
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        // 下述capacity、table、size、threshold等,用于实现类似HashMap的数据存储和扩容功能
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;
        private int size = 0;
        private int threshold; // Default to 0

        // 饱汉式,懒加载
        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);
        }

        // 用于子线程拷贝父线程的ThreadLocal对象,createInheritedMap
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (Entry e : parentTable) {
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

        // 存储KV
        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)]) { // for循环找到首个可用entry
                if (e.refersTo(key)) { // 若已有key,则更新value
                    e.value = value;
                    return;
                }

                if (e.refersTo(null)) { // 否则插入
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

        // 使用完一定要remove,否则会导致Entry持有object引用(即使key变为了null,但value仍然指向Object)
        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)]) {
                // Android-changed: Use refersTo().
                if (e.refersTo(key)) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

        // 对外暴露的set方法,将ThreadLocal对象加入到当前Thread的ThreadLocalMap中
        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                map.set(this, value);
            } else {
                createMap(t, value);
            }
        }
        // 对外暴露的get方法,可见本质上每个线程自己创建并维护了一个Object
        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();
        }
    }

ThreadLocalMap与Thread的关系

每一个Thread对象内部,都有2个Map,分别是自身的threadLocals以及继承自父亲的inheritableThreadLocals

// Thread.java

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

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Entry是WeakReference的子类

Entry是一个维护了键值对的数据包装类,Key为ThreadLocal<Object>对象,Value为该变量对应的Object对象实例。为了避免对ThreadLocal对象造成强引用,使Entry继承自WeakReference<ThreadLocal>

类HashMap实现ThreadLocal对象集合

ThreadLocalMap内部实际上维护了一个Entry[]数组,命名为table,其初始大小16,超过2/3则触发扩容。在插入时,index的计算方式为key.threadLocalHashCode & (len - 1),其中key就是ThreadLocal类型的对象。

ThreadLocal的设计思路——映射关系维护、生命周期管理

映射关系维护

根据上述分析,我们可以将ThreadThreadLocal的关系用下图表示:

ThreadLocal设计思想浅析通过阅读这篇文章,你将可以学习到: ThreadLocal的使用方法与典型应用场景 T

这里可以明确一个核心思想,即ThreadLocal机制并不是把一个对象分散给多个Thread使用,实际上它是对每个Thread都创建一个该对象实例,并且交给Thread自身进行维护。理解了这句话,也就理解了ThreadLocal机制的本质。

ThreadLocal对象设值有两种方式,一是通过覆写initialValue()函数,被动初始化;二是主动调用set()函数进行初始化。

生命周期管理

已知如上的引用关系中,存在2条引用链(这里Object对象代指ThreadLocal通过泛型包装的对象):

ThreadLocal设计思想浅析通过阅读这篇文章,你将可以学习到: ThreadLocal的使用方法与典型应用场景 T

  • Thread对象到ThreadLocal对象的虚引用:Thread->ThreadLocalMap->Entry->WeakRef<ThreadLocal>
  • Thread对象到Object对象的强引用:Thread->ThreadLocalMap->Entry->Object

建立这两种引用关系的时机很好理解,就是向ThreadLocalMap里面添加对象时,也就是覆写initialValue()或者调用set()的时机。当ThreadLocal对象使用完毕后,该Entry并不会自动移除,而是仍然保留在ThreadThreadLocalMap成员变量里(这不难理解,Thread本身又不知道这个对象后续是否要使用)。

由于Entry对于ThreadLocal对象的引用为WeakRef,当JVM发生GC时会释放该引用,从而使EntryKey变为null。在源代码里,将这种Key为nullEntry称为StaleEntry(过期条目)。很明显,过期条目持有了Object的强引用,如果不进行释放,会导致严重的内存泄漏。ThreadLocal类里面提供了expungeStaleEntry(i)函数用来清理StaleEntry

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

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            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;
        }

调用上述函数,就会遍历清理全部的StaleEntry,以下场景会触发这个流程:

  • 场景一Thread运行结束,释放ThreadLocalMap,其中的Entry自然就被释放了
  • 场景二:在调用set(Object)时,恰好命中了一个StaleEntry
  • 场景三:主动对ThreadLocal对象调用remove()函数

场景一不适用于线程池的场景,此时线程长期存活,无法释放。

场景二随机性太大,需要计算Key的hash值时恰好命中上一个被释放的Entry,不可控。

因此,最稳妥的方式是场景三。这也是在使用ThreadLocal时容易出错的地方,没有主动调用release()方法,导致有内存泄漏的风险。

ThreadLocal使用过程中的那些大坑

到这里其实也没啥好讲的,只要记住“用完要释放”,就能避免99%的问题。

转载自:https://juejin.cn/post/7410280735116083227
评论
请登录