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


         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);//获取存放的位置
    
                /**
                 * 当i位置刚好已经存储了数据
                 */
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {//向i+1<len遍历
                    ThreadLocal<?> k = e.get();//获取存入的ThreadLock
                    if (k == key) {//判断是否一致,如果一致,替换值并返回
                        e.value = value;
                        return;
                    }
    
                    if (k == null) {//如果i节点获取到的ThreadLocal为空的话               
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
                //没有存储数据的情况
                tab[i] = new Entry(key, value);//新增信的节点
                int sz = ++size;
                /**
                 * 判断是不是要进行扩容
                 * Entry数组的扩容是2倍扩容的;            
                 */
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    
    
    
         private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                           int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
                Entry e;
    
                // 判断i之前的非空节点的位置
                int slotToExpunge = staleSlot;
                for (int i = prevIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = prevIndex(i, len))
                    if (e.get() == null)
                        slotToExpunge = i;
    
                //从staleSlot又向后循环,找到非空节点,查看是否又相同key
                for (int i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) {
                    ThreadLocal<?> k = e.get();
                    /**
                     * 判断和查询的实例是否同一个,如果key相同,则和staleSlot节点进行替换位置   
                     */ 
                      if (k == key) {
                        e.value = value;
    
                        tab[i] = tab[staleSlot];
                        tab[staleSlot] = e;
    
                        if (slotToExpunge == staleSlot)
                            slotToExpunge = i;
                        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//这里进行rehash
                        return;
                    }
    
                    if (k == null && slotToExpunge == staleSlot)
                        slotToExpunge = i;
                }
    
                // 如果key没找到那就在staleSlot位置插入一个新节点
                tab[staleSlot].value = null;
                tab[staleSlot] = new Entry(key, value);
    
                // 判断是不是要重新尽心hash
                if (slotToExpunge != staleSlot)
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            }
    
    

    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值为当前的ThreadLocal实例,所以一般要求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. 避免了开发人员直接操作当前线程,而使用“变量副本”进行操作
    
    
  • 相关阅读:
    第五周
    第三章 程序的机器级表示
    第二章 信息的表示和处理
    嵌入式Linux应用开发——Linux下的C编程基础
    Linux基础入门(20135207 王国伊)
    Java实验报告(实验四)
    linux系统之pam模块
    linux 从入门到跑路-时间,日期问题
    linux 从入门到跑路-Shell和部分命令
    linux 从入门到跑路-挂载,命令的执行顺序
  • 原文地址:https://www.cnblogs.com/perferect/p/13646131.html
Copyright © 2020-2023  润新知