并发编程-ThreadLocal解析

       首先,在解析ThreadLocal之前,我们首先要知道这东西是个什么玩意儿,ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

      下面,我来手写一个ThreadLocal的实现来理解一下它的原理:

public class ThreadLocal<T>
{
    private Map<Thread,T> threadMap = new ConcurrentHashMap<Thread,T>();

    public void set(T value)
    {
        Thread thread = Thread.currentThread();
        this.threadMap.put(thread,value);
    }

    public T get()
    {
        Thread thread = Thread.currentThread();
        if(this.threadMap.containsKey(thread))
        {
            return this.threadMap.get(thread);
        }
        return null;
    }

    public void remove()
    {
        Thread thread = Thread.currentThread();
        if( this.threadMap.containsKey(thread))
        {
            this.threadMap.remove(thread);
        }
    }

}

看了上边的代码,我们可以看出,ThreadLocal其实就是一个类似于map的存储结构,只不过在源码中,它是一个线程私有的局部变量,而不是咱们定义的threadMap。

 

源码解析

那么我们对照上边自己写的ThreadLocal,来看看JDK源码里是怎么实现的:

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

获取Thread实例,然后将ThreadLocal实例作为key放入一个map中

我们再来看看getMap()这个方法

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

实际上getMap方法就是返回的Thread的一个成员变量,那么这个成员变量是一个ThreadLocalMap类型的对象,我们再来看看

ThreadLocalMap是个什么东西

static class ThreadLocalMap {

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

那么ThreadLocalMap的底层是一个Entry的静态类数组,实现了一种键值对的数据结构,我们常用Map的底层也是这种数据结构吧,无非就是这里的key值是一个弱引用,这个我们呆会儿再讲,那么现在我们是不是就可以画出ThreadLocal的数据结构了?

 

        那么我们在使用ThreadLocal的set方法时,它首先会获取当前线程的threadLocals这个成员变量,如果map不为空,则将当前ThreadLocal实例+value放入Entry中,一个threadLocals有N个Entry,所以一个线程中可以new多个ThreadLocal实例来存放不同数据,一个ThreadLocal实例就对应map中一个key,当然你也可以把value打包成一个map,然后用一个ThreadLocal实现也是莫得问题滴。

       如果获取的线程map为空,那么set方法会调用createMap方法

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

    private static final int INITIAL_CAPACITY = 16;
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

createMap就是创建一个初始大小为16的Entry数组,通过用firstKey的threadLocalHashCode与初始大小16取模得到哈希值,得到数组的下标位置,然后初始化对应下标的Entry对象,setThreshold(INITIAL_CAPACITY)就是说设置扩容阈值为容量的三分之二,扩容数是原来的2倍,扩容方法是resize()。那么int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);这段代码的意思就是说计算的下标值可以更均匀的分布在map中,扩容是2的幂次方也是更加能优化性能,因为ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。

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

这个是get方法,其实就是跟set的逻辑差不多,最终是从Entry数组中取到对应得key值,经过运算,如果key值能直接命中,则返回value,否则使用线性探测法来寻找key值

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            //获取当前ThreadLocalMap中的数组
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                //再次判断是否命中key值,若命中则返回
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
             //该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry,这个跟弱引用有关
                    expungeStaleEntry(i);
                else
                    //进行线性寻址
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

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

nextIndex的意思就是说从当前下标往下依次查找,如果Entry[i]的key没有命中,则找Entry[i+1],直到找到Entry[INITIAL_CAPACITY-1],如果还没有就返回null。

public void remove() {
         //得到当前线程的ThreadLocalMap 
         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;
                 //采用线性寻址法,依次判断是否命中key值
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    //清理弱引用,将Entry的key值置null
                    e.clear();
                    //执行清理
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

remove方法寻找对应key值的方式和getEntry是一样的,最后就是清理一下。

 

ThreadLocal内存泄漏分析

上边我们有看到,Entry对象的key值是一个弱引用,这就涉及到了ThreadLocal内存泄漏的问题,什么是内存泄漏?内存泄漏就是在程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。

看下这个示意图,虚线部分就是一个弱引用,ThreadLocalRef和CurrentThreadRef是线程私有栈中对ThreadLocal实例和Thread实例的引用,这是一个强引用,ThreadLocal和CurrentThred则是代码中new ThreadLocal实例和当前线程的实例,Map则是线程实例的threadLocals成员变量,也就是上边咱们说的ThreadLocalMap,那么什么是否会发生内存泄漏呢?

当ThreadLocal用完了之后,1引用就断了,此时jvm进行GC回收的时候,弱引用的对象ThreadLocal就被回收了,也就是说此时,这个Entry实例的key值没有了,这是一个无效的Entry实例了,但是3,4,5这个引用链还是强引用,没有断,也就是说此时这个无效的Entry实例无法即时被GC回收,这样就造成了一种内存泄漏。

为了解决这种问题,ThreadLocal有一套自我清理的机制,当然这纯粹是开发JDK的大佬们在给我们不规范的写代码擦屁股,如果我们ThreadLocal调用完成,即时用remove方法清理,就不会存在内存泄漏问题。那么自我清理机制是什么呢?小伙伴们可以去看看源码,跟踪一下,get,set,remove方法,逻辑里都会有expungeStaleEntry(int staleSlot)这个方法,这个就是大佬们提供的清理Entry中的value的方法:

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

            // expunge entry at staleSlot
            //首先清理数组下标对应的key/value信息
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            //重点:会根据线性寻址法,依次获取当前下标之后的Entry,如果Entry的key值为null,则清理value
            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;
        }

expungeStaleEntry方法就是尽量的清除掉无效的value,但是除了remove能够百分百调用这个方法外,get和set方法都不一定能够即时调用这个方法来清理无效Entry,所以还是会存在内存泄漏的情况,当然线程如果被回收,那么数组中所有的东东也就被回收掉了,当然如果是线程池+ThreadLocal的情况就是大写的杯具了!

从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
下面我们分两种情况讨论:
key 使用强引用:对ThreadLocal 对象实例的引用被置为null 了,但是ThreadLocalMap 还持有这个ThreadLocal 对象实例的强引用,如果没有手动删除,ThreadLocal 的对象实例不会被回收,导致Entry 内存泄漏。
key 使用弱引用:对ThreadLocal 对象实例的引用被被置为null 了,由于ThreadLocalMap 持有ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的对象实例也会被回收。value 在下一次ThreadLocalMap 调用set,get,remove 都
有机会被回收。
比较两种情况,我们可以发现:由于ThreadLocalMap 的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。
因此,ThreadLocal 内存泄漏的根源是:由于ThreadLocalMap 的生命周期跟Thread 一样长,如果没有手动删除对应key 就会导致内存泄漏,而不是因为弱引用。

ThreadLocal线程不安全

兄弟,看见这个标题,你是不是懵逼了?ThreadLocal不就是来解决线程不安全性问题的吗?

如果ThreadLocal正确使用的话,肯定是能够保证线程安全的,但是执行下以下代码:

public class ThreadLocalUnsafe implements Runnable {

    public Number number = new Number(0);

    public void run() {
        //每个线程计数加一
        number.setNum(number.getNum()+1);
      //将其存储到ThreadLocal中
        value.set(number);
        SleepTools.ms(2);
        //输出num值
        System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
    }

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnsafe()).start();
        }
    }

    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }

        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }

}

这种情况下,ThreadLocalMap保存的是Number对象的一个引用,那么只要这个对象里的num变化了,是不是所有的ThreadLocalMap中的Number的num都改动了,这种情况就造成了线程不安全了。如果要把它改成线程安全的,则只需要将number对象放入前,new一下就可以了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章