ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

ThreadLocal总结

2022-06-13 22:35:28  阅读:153  来源: 互联网

标签:总结 ThreadLocalMap value ThreadLocal 线程 key null


ThreadLocal

1、ThreadLocal能干嘛

ThreadLocal是能够在线程上下文中保存数据的,保证每个线程都能够访问到自己当前线程中独有的数据

避免当前线程访问到其他线程的数据。

如上所示,安琪拉的请求数据只会由ThreadA处理,妲己的请求数据只会由ThreadB处理。

保证数据不会串行。

2、ThreadLocal怎么实现的

理解实现之前,先熟悉一下三个类:Thread、ThreadLocal、ThreadLocalMap

Thread:

线程,这个再简单不过。

ThreadLocal

设计目的就是为了让当前线程携带数据。

ThreadLocalMap

设计的目的就是为了维护当前线程的本地变量而设计的

三者之间的关系

1、Thread是携带数据的载体,因为数据需要在线程上下文中流转;

2、ThreadLocal就是对保存在Thread上下文的数据进行操作的;

3、ThreadLocalMap是保存数据的结构;

ThreadLocalMap是一个存储的数据结构;ThreadLocal是操作数据的动作;而Thread则是操作数据的载体;

ThreadLocal可以操作ThreadLocalMap在线程创建之后、销毁之前,在线程中存储、取出、销毁数据。

三者之间的结构

Thread中声明ThreadLocalMap

ThreadLocal.ThreadLocalMap threadLocals = null;

而ThreadLocalMap确实ThreadLocal中的内部类

为什么要这样子来进行设计?

因为对于线程来说,在线程上下文中没有必要一定要携带数据。有时候需要携带,有时候不需要携带。

那么定义在Thread中是没有必要的,所以这也就是为什么说在Thread中只是声明而已。

那么为什么要在ThreadLocal中来进行定义?

因为ThreadLocalMap的设计意义就在于维护线程的本地变量,这里做的事情仅仅只是存储。

而ThreadLocal是来操作当前线程携带的数据的。

目的就是一个:能够让线程携带上数据,也能够让线程不携带数据。

问题

为什么要使用ThreadLocal来作为ThreadLocalMap的key,而不是Thread来作为key

可能Thread来作为key对于新手来说是更合理的。

但是这里想一个问题:对于一个线程来说,不可能只保存一条数据,可能是多个,也可能是不保存数据。

那么如果利用线程作为key,可以保存数据;那么要存储多条数据的时候如何保存呢?

线程要保存数据A,key是当前线程对象;线程要保存数据B,key是当前线程对象的话,因为底层是map结构,map结构继续put的时候是会覆盖掉原来的值的。

那么这样子将会导致整个线程只能携带一个数据,而不是多个数据。

而利用ThreadLocal来作为key,只需要在保存数据的时候创建一个新的ThreadLocal对象即可。保存多个数据,只要多个ThreadLocal对象即可。

源码篇

我觉得对于这里比较绕的东西,过多的解释不如看源码。

通过一个小案例来进行讲解:

public class ThreadLocalUtil {

    private static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>();


    public static void setUserThreadLocal(User user){
        USER_THREAD_LOCAL.set(user);
    }

    public static User getUser(){
        return USER_THREAD_LOCAL.get();
    }

    public static void remove(){
        USER_THREAD_LOCAL.remove();
    }

}

我们都知道对应的使用顺序:set--->get-->remove

那么底层到底做了什么?

set方法

直接看源码:

    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。

偷偷瞄一眼获取得到ThreadLocalMap的方法

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

因为上面也看到了,声明的时候是为null的。所以判断如果是null的时候,那么接下来就是要来创建当前Thread线程对象的ThreadLocalMap了

看看具体的创建过程:

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

这里也很简单,就是直接创建了一个ThreadLocalMap。

对于线程来说,因为创建了ThreadLocalMap之后,下次再来放入数据的时候,因为这个ThreadLocalMap已经存在了,所以做的事情就是继续添加,这个很好理解。

注意点

注意在构造函数中,this代表的是当前对象,也就是threadlocal对象,也就是调用set方法的threadlocal对象。

看看这里的构造方法和上面的set方法是不是最相似?其实是一样的操作。

那么接下来看下构造中做了什么,但是这就涉及到了ThreadLocalMap的设计,所以这个放在下面讲。

get方法

    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的时候,已经创建过了ThreadLocalMap,直接获取得到即可。

获取得到ThreadLocalMap之后,那么接下来就应该获取得到我们存入到线程中的数据了。

因为放入进去的key是threadlocal对象,所以这里get的时候,也是通过threadlocal对象获取得到的,然后转换返回得到对象。

但是这里的判断操作,说明没有得到对应的map。那么也就只有一种情况了,没有经过set创建map,而是直接经过get来进行操作,获取得到对应的值。没有经过初始化创建对应的map,那么也就没有对应的value值了。

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

第一行就是ThreadLocal中的初始化方法:

protected T initialValue() {
    return null;
}

也就是说,一个类继承ThreadLocal类重写了initialValue方法,那么也会把值设计到ThreadLocalMap中来。

但是通常不会来这样子操作。这里只是留了一个点,让开发者自己来进行实现。

remove方法

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

这里也是十分简单的,直接将key移除掉,下次再来根据ThreadLocal来进行找的时候就找不到对应的value了。

那么要是按照这样子想的话,那也太不把设计源码的人当回事了吧。

下来来看看ThreadLocalMap的设计

ThreadLocalMap

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

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

可以看到在ThreadLocalMap中的key-value组成的对象是Entry,每个对象一组。而这里的K又继承了WeakReference若引用。

若引用表示的是在JVM运行的任意期间都可以销毁掉。那么这里的K为什么要来集成若引用?

那么这里通过图来看比较合适:

key是强引用

在线程运行期间,threadlocal引用的生命周期比较短,那么肯定是先消失,而堆中的引用还在。而因为线程对象还在,意味着threadlocalmap还在,那么key将会执行堆中的threadlocal对象,那么就会导致了threadlocal无法被JVM回收,将会导致内存泄漏。

而如果线程的生命周期也很短,那么将没有这个内存泄漏问题。最终都会被回收掉。

而我们经常在web开发中使用到thread,而web中内嵌了一个线程中,那么将会导致线程无法销毁,那么将会出现内存泄漏问题。

所以如果key是强引用+web开发中,那么内存泄漏是一定的。

key是弱引用

弱引用表示的是JVM随时都可以回收掉这个对象。

那么当threadlocal引用生命周期消逝了之后,map中的key弱引用到了threadlocal在堆中的地址,那么显然key是会被回收掉的。

如果thread生命周期短,那么也不会出现问题。

但是如果是在web开发模式下,thread是在线程池中的,不会消失的。那么将会导致这里的value永远无法获取得到。

而导致了value的内存泄漏。

解决办法

那么解决办法是什么?????

手动的将value给清除掉。而remove方法就是来做这个事情的。因为上面没有分析remove方法的具体实现,下面来看看:

        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) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

看到第三行源代码的时候,应该很熟悉了。线性探测法,找到threadlocal在数组中的下标,因为可能会涉及到hash碰撞,所以这里也有考虑。

而根据key获取得到value的时候,这里的操作可以看下:

               if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }

首先将这个entry置空,也就是说threadlocalmap中数组指定位置置为空。那么threadlocal和value还存在,因为threadlocal是弱引用,可以被回收掉,而value可是强引用。那么应该来对value做一下操作。而expungeStaleEntry方法就是来做这个事情的:

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

而我们首先可以看到已经将value置为了null。其实下面的操作无非就是遍历整个数组,做检查。因为JVM回随时回收掉若应用,这里就是不断的来进行检查。

我们试想一种场景,在进行set未进来remove的阶段中,key被JVM回收了。

那么接下来我们进行get、remove时候发生的事情。

进行get方法的时候:

    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还在,所以可以得到对应的值,而k被回收了,就意味着找不到对应的threadlocal了,那么里面的value怎么办?

肯定是在threadlocal还在的时候,那么就可以通过threadlocal来获取得到这里的value的值。

所以不会存在在set、remove之间得不到对应的value的情况。

总结

1、三个类之间的关系?为什么要这样子设计?

2、为什么要利用Threadlocal作为key?

3、怎么才不会导致内存泄漏?

4、set和remove之间不会得不到对应的value;

标签:总结,ThreadLocalMap,value,ThreadLocal,线程,key,null
来源: https://www.cnblogs.com/likeguang/p/16372556.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有