JAVA并发编程---ThreadLocal底层原理全解析

简介

ThreadLocal虽然在工作中使用的场景可能并不是很多,但是还是很值得我们去了解和学习一下的。在特定场景下它能发挥出很大的作用,首先,让我们来了解下什么是ThreadLocal。ThreadLocal就是我们俗称的线程本地变量,它常用来做数据隔离,填充的数据只属于当前线程。变量的数据对别的线程而言是相对隔离的,在多线程环境下,能够防止自己的变量被其它线程篡改。在之前一篇对springMvc中的Req uestContextHolder学习的文章中就有涉及到ThreadLocal ,《springMvc—RequestContextHolder分析》在RequestContextHolder中正是使用了ThreadLocal 来存储我们当前线程的request对象。ThreadLocal 在源码中使用到的场景还有很多如我们最常使用到的@Transactional (Trans actionSynchronizationManager)事务注解以及在Slf4j中多线程间日志隔离的实现等。

实现原理

既然我们知道了ThreadLocal是存储在线程中的本地变量,那为什么线程中能存放ThreadLocal又是怎么存放的呢。我们首先通过Thread Local中的set()方法看看是否能找到答案,因为存值正是通过这个方法我们只需要顺藤摸瓜大概率就能找到我们想要了解的真相。

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread(); // 获取当前线程
        ThreadLocalMap map = getMap(t); // 获取ThreadLocalMap对象
        if (map != null) // 校验对象是否为空
            map.set(this, value); // 不为空set
        else
            createMap(t, value); // 为空创建一个map对象
    }

set方法的源码十分简单,可能最让人迷惑的就是这个ThreadLocalMap 对象。它到底是做什么用的又是从何获取到的?我们进入getMap方法看一下:

 /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

原来ThreadLocalMap 是内置在Thread类中的一个成员变量
在这里插入图片描述
这里我们基本上可以找到ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建Thread Local的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。

ThreadLocalMap底层结构

在了解了ThreadLocal大致原理后,我们需要更加深入的进入ThreadLocalMap中看看到底是如何进行数据存储的。里面还有很多的细节值得我们去学习。首先ThreadLocalMap是ThreadLocal类的一个静态内部类
在这里插入图片描述
虽然它名字中包含Map但是却没有什么关系,它其实内部是有个Entry数组,将数据包装成静态内部类Entry对象,存储在这个table数组中。
在这里插入图片描述
下面我们一起具体看一下Entry这个内部类到底有什么猫腻,首先最直观的就是它继承自WeakReference,在构造函数中将值传给了父类。这样设置的值就成为了ThreadLocal的弱引用对象。至于弱引用对象跟我们最常见new出来的强引用对象有什么区别?我们知道 ,弱引用对象在Java虚拟机进行垃圾回收被扫描到时就会被释放,不管当前内存空间足够与否都会回收它的内存(不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象)。至于为什么不直接用强引用而是使用弱引用呢这个我们后面说,首先我们这里引出一个关于ThreadLocal使用不当会产生 内存泄漏的问题。ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建Thread Local的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。所以最好的做法是即使调用threadlocal的remove方法:把当前ThreadLocal从当前线程的ThreadLocalMap中移除。

从上面我们可以看出被WeakReference引用的对象就是ThreadLocal本身,也就是说ThreadLocal自身的回收不受ThreadLocalMap的这个弱引用的影响,让用户减轻GC的烦恼。至于为什么将ThreadLocal设计为弱引用,理解了WeakReference之后,ThreadLocalMap使用它的目的也相对清晰了:当threadLocal实例可以被GC回收时,系统可以检测到该threadLocal对应的Entry是否已经过期(根据reference.get() == null来判断,如果为true则表示过期,程序内部称为stale slots过期的插槽)来自动做一些清除工作,否则如果不清除的话容易产生内存无法释放的问题:value对应的对象即使不再使用,但由于被threadLocalMap所引用导致无法被GC回收。

在这里插入图片描述

ThreadLocal是如何做自动清除工作的?

在ThreadLocalMap中有一个expungeStaleEntry方法,这个方法在ThreadLocalMap中,执行 get、set、remove、rehash等方法时都直接或间接调用到它会自动清除掉table数组中过期的成员。这样entry在后续的GC中就会被回收。下面我们随便找一个get方法看一下:
在这里插入图片描述
通过key.threadLocalHashCode & (table.length - 1)来计算存储key的Entry的索引位置,然后判断对应的key是否存在,若存在,则返回其对应的value,否则,调用getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法
在这里插入图片描述
ThreadLocalMap采用线性探查的方式来处理哈希冲突,所以会有一个while循环去查找对应的key,在查找过程中,若发现key为null,即通过弱引用的key被回收了,会调用expungeStaleEntry(int)方法
在这里插入图片描述
我们可以清晰的看到在key为null的时候,expunge entry at staleSlot会直接删除掉当前数组处的值。此时,CurrentThreadRef不存在一条到Entry对象的强引用链,Entry到value对象也不存在强引用,那在程序运行期间,它们自然也就会被回收。expungeStaleEntry(int)方法的后续代码就是以线性探查的方式,调整后续Entry的位置,同时检查key的有效性。

在ThreadLocalMap中的set()/getEntry()方法中,都会调用expungeStaleEntry(int)方法,但是如果我们既不需要添加value,也不需要获取value,那还是有可能产生内存泄漏的。所以很多情况下需要使用者手动调用ThreadLocal的remove()函数,手动删除不再需要的Thread Local,防止内存泄露。若对应的key存在,remove()方法也会调用expungeStaleEntry(int)方法,来删除对应的Entry和value。其实,最好的方式就是将ThreadLocal变量定义成private static的,因为静态变量在内存中只有一个,放在方法区内,属于类变量只有类在被卸载时才会被回收。这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,可以防止内存泄露。

  /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null; // 将ThreadLocal的值设weinull
            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;
        }

如何共享线程的ThreadLocal数据?

在结尾我们学习最后一个知识点,使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个Inherita bleThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值是可以获取到的。那么它是如何实现这个功能的呢?
首先,InheritableThreadLocal继承自ThreadLocal,使用InheritableThreadLocal类可以使子线程继承父线程的值。如果细心的人会发现在我上面关于ThreadLocalMap 的截图中threadLocals变量下面就是inheritableThreadLocals。InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中createMap方法在被调用(当前线程调用set方法时得到的map为null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。
在这里插入图片描述
Thread源码中,我们看看Thread.init初始化创建的时候做了什么:

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    //判断名字的合法性
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;
    //(1)获取当前线程(父线程)
    Thread parent = currentThread();
    //安全校验
    SecurityManager security = System.getSecurityManager();
    if (g == null) { //g:当前线程组
        if (security != null) {
            g = security.getThreadGroup();
        }
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    g.checkAccess();
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g; //设置为当前线程组
    this.daemon = parent.isDaemon();//守护线程与否(同父线程)
    this.priority = parent.getPriority();//优先级同父线程
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    //(2)如果父线程的inheritableThreadLocal不为null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //(3)设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    this.stackSize = stackSize;

    tid = nextThreadID();
}

在init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的inheritableThreadLocals是否为null,然后调用createInheritedMap将父线程的inheritableThreadLocals作为构造函数参数创建了一个新的ThreadLocalMap变量,然后赋值给子线程。下面是createInheritedMap方法和ThreadLocalMap的构造方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        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++;
            }
        }
    }
}

在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值到新的ThreadLocalMap对象中。返回之后赋值给子线程的inheritableTh readLocals。总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritable ThreadLo cals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变 量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inher itableThreadLocals变量中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值