当前位置 博文首页 > 文章内容

    线程基础知识08- ThreadLocal基础总结

    作者: 栏目:未分类 时间:2020-09-11 16:01:02

    本站于2023年9月4日。收到“大连君*****咨询有限公司”通知
    说我们IIS7站长博客,有一篇博文用了他们的图片。
    要求我们给他们一张图片6000元。要不然法院告我们

    为避免不必要的麻烦,IIS7站长博客,全站内容图片下架、并积极应诉
    博文内容全部不再显示,请需要相关资讯的站长朋友到必应搜索。谢谢!

    另祝:版权碰瓷诈骗团伙,早日弃暗投明。

    相关新闻:借版权之名、行诈骗之实,周某因犯诈骗罪被判处有期徒刑十一年六个月

    叹!百花齐放的时代,渐行渐远!



         ThreadLocal在平时开发中是用的比较多的一个类,主要用于存储线程的数据。下面我对ThreadLocal进行一下总结;

         dreamcatcher-cx大佬对ThreadLocal的设计总结,写的比较深刻,很有帮助。

    主要使用场景:

    • 多数据源切换,记录当前线程访问的数据源

    • spring框架事务管理,用于存放事务数据;

    • springsecurity安全框架,用于存储用户登录信息;

    除了以上还有很多,不限于以上。

    解读源码

    数据存储ThreadLockMap

    • 通过Entry数组进行存储的;

    • Entry继承Reference类,是弱关联的类,当ThreadLocal的实例为空时,GC会快速收回;一般用于处理数据量较大且维持时间较短的业务。

    • Entry节点存储的是ThreadLocal对象和对象值

    //是通过Entry数组进行存储的 
    private Entry[] table;
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
                Object value;
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    

    set 方法

    • 存放ThreadLocal<?>的是弱关联的容器,GC会等存储满的时候处理;

    • 存放的过程中,会对key判断,调整数组位置;

     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)]) {
                    ThreadLocal<?> k = e.get();//获取存入的ThreadLock
                    if (k == key) {//判断是否一致,如果一致,替换值并返回
                        e.value = value;
                        return;
                    }
    
                    if (k == null) {//如果i节点获取到的ThreadLocal为空的话
                        /**
                         * 1.从i 的向前循环查找tab[i]节点的ThreadLocal不为空的位置slotToExpunge;
                         * 2.从i以下节点循环遍历,会有两种情况;
                         * 3.第一种情况:找到对应key相等的节点,和当前遍历的节点位置进行交换;如果slotToExpunge和i节点相同,则进行rehash
                         * 4.第二种情况:key为空,并且slotToExpunge和遍历的位置相等的情况下,slotToExpunge == 遍历的位置;  
                         * 5.根据传入的i和slotToExpunge是否相等,进行数组的节点的清除和rehash操作   
                         */                
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    
                tab[i] = new Entry(key, value);//新增信的节点
                int sz = ++size;
                /**
                 * 判断是不是要进行扩容
                 * Entry数组的扩容是2倍扩容的;            
                 */
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    

    get方法

    • 获取存储的Entry节点
       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
                    /**
                     * 从i位置往后逐个遍历查找如果找到了就返回,没找到返回null;
                     */   
                    return getEntryAfterMiss(key, i, e);
            }
    
    

    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类的方法

    set方法数据存储

    • 首次存储,新建一个ThreadLockMap,以当前ThreaLock为key;

    • 如果线程的存储区域已经初始化过,则更新存储区域中的数据;

    public void set(T value) {
            Thread t = Thread.currentThread();// 获取当前线程
            ThreadLocalMap map = getMap(t);//获取存储数据的MAP
            if (map != null)//判断存储数据的map是否为空,如果为空则创建map,如果不为空,则存入数据
                map.set(this, value);
            else
                createMap(t, value);//如果没有就创建一个,当前Thread
        }
    //获取当前线程的值存储区域
    ThreadLocalMap getMap(Thread t) {
            return t.threadLocals; //线程的成员变量
        }
    //给当前线程创建一个新的ThreadLocalMap,并存储以当前ThreaLocal实例为key的键值对。
     void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
    

    get方法

    • 如果在访问之前没有存储任何数据,则对ThreadLockMap进行初始化,绑定当前线程。并进行初始值的创建,和当前ThreadLock实例进行绑定,kv存储

    • 如果获取到之前存储的值,则进行返回存入的值;

    public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);//获取当前线程的存储map
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);//获取当前的ThreadLock的节点,具体上面有介绍
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();//如果不存在则进行初始化
        }
    
    
    //进行一些初始化操作
    private T setInitialValue() {
            T value = initialValue();//ThreaLock提供的一个可继承的方法,进行初始化操作
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);//获取当前线程的
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
        }
    
    

    remove 方法

      public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)//在存储的map不为空的情况下,移除当前ThreadLock实例
                 m.remove(this);
         }
    
    

    总结

    注意点:

    1. ThreadLocalMap存储的key值为当前的ThreadLock实例,所以一般要求ThreadLocal是单例的
    
    2. 单个线程可以有多个ThreadLocal实例,存储数据。
    

    总结分析:

    1.ThreadLocal的设计思路:

    • 原理:相当给当前线程的创建独有数据域,方便当前线程访问。这样避免了多线程环境中,访问当前线程锁的相关问题

    • 之所以使用ThreadLocalMap进行存储,而不用HashMap,上面源码分析说了,源于Reference类的特性。

      • 弱应用,当对象无用的时候,会快速被GC回收。符合线程的特点,一般一个线程时间不会很长,当伴随大量数据时能快速回收

      • 当前线程结束的时候,要通过调用ThreadLocal的remove方法,即使将ThreadLocalMap中的对应实例key设置为空,方便GC快速回收

    • ThreadLocal操作更有优势,下面我具体分析以下

    为什么用ThreadLocal类直接操作存储数据 ?

    1.简化代码:

    试想一下上面的源码,如果直接从Thread中存放数据,
    首先,要用Thread.currentThread()获取当前线程,
    然后,再通过当前线程thread获取存储的Map
    再根据key获取对那个的操作
    
    从上面的源码可以看出,这些操作细节,都已经在ThreadLocal类中进行了隐藏;
    1.避免每次使用时的重复代码;
    2.而使用ThreadLocal实例获取数据,相当于拿key直接获取数据,隐藏了获取细节
    
    

    2.限制和统一管理存储值

    1. 内部的Entry数组,进行了泛型约束,避免了直接在Thread类中Map操作key的不规范性;
    
    2. 避免了开发人员直接操作当前线程,而使用“变量副本”进行操作