• java基础之:聊ThreadLocal类,以及其解决内存泄漏做法


    ThreadLocal是Thread的局部变量,用于编多线程程序,对解决多线程程序的并发问题有一定的启示作用。

    看下官网文档解释:

    这个类提供线程局部变量。  这些变量与其正常的对应方式不同,因为访问一个的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。  ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)。 
    
    • 1

    简单的说就是:可以为某个线程提供一个局部变量,而这个变量在多线程的环境下访问时能够保证各个线程中某个数据的独立型。说白了就是为独立线程绑定自用的独立数据。

    当然其有一些特点如下:

    • 其实通过ThreadLocal为线程关联的数据,有点像是Map一样,只不过其key时当前线程。为什么这样说后面演示就明白。
    • 每个一个ThreadLocal对象只能为当前的线程关联一个数据,如果为当前数据关联多个数据,那么不好意思,就需要需要多个ThreadLocal对象实例进行关联。
    • ThreadLocal对象实例的时候一般都是static类型的,毕竟方便调用。
    • ThreadLocal保存的数据,在线程销毁后由JVM虚拟机字段释放。

    看一下其构造方法和方法,官网截图如下:

    在这里插入图片描述

    初体验

    用map模拟threadLocal

    前面一直说很像是map,那自然也可以通过map来模拟。

    如果不不了解多线程的话,可以看下线程文档-传送阵

    现在开始体验:

    package com.test;
    
    import java.util.HashMap;
    import java.util.Map;
    
    
    public class test_thread implements Runnable {
        public static Map<Object, Object> threadMap = new HashMap<Object, Object>();
    
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            int i = (int) Math.floor(Math.random() * 100);
            System.out.println("绑定前   的线程【"+threadName+"】"+i);
            threadMap.put(threadName,i);
            new testDAO().dao();
    //        这里可以直接在这个线程进行演示,不过为了解决歧义直接引入一个对象吧
    //        System.out.println("绑定后的线程【"+threadName+"】"+threadMap.get(threadName));
        }
    
        public static void main(String[] args) {
            test_thread test=new test_thread();
            for (int i=0;i<3;i++){
                new Thread(test).start();
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    public class testDAO {
        public  void dao() {
            String threadName = Thread.currentThread().getName();
            System.out.println("testDAO--中--绑定后的线程【" + threadName + "】" + test_thread.threadMap.get(threadName));
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    似乎也可以解决这个问题,不过其本质差不多也是这个原理而已。不过使用threadLocal可能会方便,而失去人家设计的逻辑也会减少内心泄漏的风险,只能说减少而不是完全解决因为传递参数而造成的内存泄漏.

    体验threadlocal

    package com.test;
    
    import java.util.HashMap;
    import java.util.Map;
    public class test_thread implements Runnable {
        public static ThreadLocal threadLocal = new ThreadLocal();
        //    如果多个数据的话,需要在创建一个threadLocal
        public static ThreadLocal threadLocal1 = new ThreadLocal();
    
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            int i = (int) Math.floor(Math.random() * 100);
            System.out.println("绑定前   的线程【" + threadName + "】" + i+"还有一个 "+i+"_"+i);
    //       同一个threadLocal有多个set的话其内容时最后一个set的内容
            threadLocal.set(i);
            threadLocal1.set(i+"_"+i);
            new testDAO().dao();
        }
        public static void main(String[] args) {
            test_thread test = new test_thread();
            for (int i = 0; i < 3; i++) {
                new Thread(test).start();
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    public class testDAO {
        public  void dao() {
            String threadName = Thread.currentThread().getName();
            System.out.println("testDAO--中--绑定后的线程【" + threadName + "】" + test_thread.threadLocal.get()+"   "+test_thread.threadLocal1.get());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    可以看出使用threadlocal来绑定数据的话可以看出其不需要输入key值。至于为什么后面通过源码解析来聊这个问题.

    方法

    方法描述
    protected T initialValue()返回此线程局部变量的当前线程的“初始值”。 该方法将在第一次使用get()方法访问变量时被调用,除非线程先前调用了set(T)方法,在这种情况下, initialValue方法将不会被调用。 通常情况下,这种方法最多每个线程调用一次,但它可能会再次在以后的调用的情况下调用remove()其次是get() 。
    public T get()返回当前线程的此线程局部变量的副本中的值。 如果变量没有当前线程的值,则首先将其初始化为调用initialValue()方法返回的值。
    public void set(T value)将当前线程的此线程局部变量的副本设置为指定的值。 大多数子类将无需重写此方法,仅依靠initialValue()方法设置线程本地值的值。
    public void remove()删除此线程局部变量的当前线程的值。 如果此线程本地变量随后是当前线程的read ,则其值将通过调用其initialValue()方法重新初始化 ,除非其当前线程的值为set 。 这可能导致当前线程中的initialValue方法的多次调用。

    四个方法中,**最常用的是set和get方法,不过一般为了防止内存溢出在使用完后记得调用remove方法。**方法不在演示了,毕竟很容易理解。

    源码解析

    还是老规矩,既然要了解这个类还是需要看源码的,既然看源码,还是和看集合一样,通过添加数据作为切入口.也就是set方法:

    set方法

    在这里插入图片描述

    然后依行聊一下这个代码:

     public void set(T value) {
         // 可以看出 t 就是当前线程本身
            Thread t = Thread.currentThread();
         // 这个地方需要看一下 ThreadLocalMap 这个其实是一个内部类 看下面的图
         // 这个 getMap 得到ThreadLocalMap 然后看一下这个get方法
            ThreadLocalMap map = getMap(t);
         //  如果有这个ThreadLocalMap容器就直接放入数据
            if (map != null)
                // 可以看出为什么像是map却没有在使用的时候没有使用key,而是放入了this 而这个this之的就是当前ThreadLocal这个对象
                map.set(this, value);
            else
                // 没有的话就需要创建ThreadLocalMap这个容器
                createMap(t, value);
        }
    
    //   可以看出这个 ThreadLocalMap  是线程上的一个叫做threadLocals的属性  然后看下thread 类中的确有叫做threadLocals的ThreadLocalMap对象
     ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
    // 但是如果线程上的threadLocals 返回的是null 就需要创建这个ThreadLocalMap对象
     void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在这里插入图片描述

    在这里插入图片描述

    从上面的三个方法中可以看出如下:

    • 首先获取当前线程,而通过当前线程的threadLocals属性得到ThreadLocalMap这个容器 其实也证明了一点线程上可以带有多个ThreadLocal数据,因为其存储在一个类似map的容器里面.
    • 如果ThreadLocalMap这个容器不为null,就将参数放入其中,而当前ThreadLocal(通过this)作为key.毕竟是同set方法进行切入的,所以这个没有疑问.
    • 如果通过线程得到的ThreadLocalMap这个容器为null,那么就需要创建一个ThreadLocalMap这个对象,并添加数据.

    现在就看一下ThreadLocalMap这个到底是什么鬼?

    ThreadLocalMap

    在这里插入图片描述

    在这里插入图片描述

    第一次创建ThreadLocalMap对象

    这个ThreadLocalMap是一个内部静态类,现在看一下其构造方法.

     ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
         //  这个Entry熟悉吗因为map也有一个Entry对象
                table = new Entry[INITIAL_CAPACITY];
         //  计算索引  这个也是方便找查找所在
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
         //  其实有个一个Entry数组 
                table[i] = new Entry(firstKey, firstValue);
         // 因为第一次添加  所以内容值为1
                size = 1;
         // 设置阈值 也就是扩容的标识符threshold的值
                setThreshold(INITIAL_CAPACITY);
            }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    其实上面所写的大写字符串代表什么或者说是什么值:

    // 默认长度也是16       
    private static final int INITIAL_CAPACITY = 16;
    // Entry数组   
    private Entry[] table;
    // Entry数组table中内容有多少个 默认是0
    private int size = 0;
    // 这个是一个 这个是内容扩展的阀门 默认是零 扩展的时候是数组长度而不是内容长度乘以2除以3
    // private void setThreshold(int len) {   threshold = len * 2 / 3; }
    private int threshold; // Default to 0
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    ThreadLocalMap扩容

    判断threadlocalmap的不为null,那就是调用的是threadlocalmap中的set方法:

    private void set(ThreadLocal<?> key, Object value) {
    				
                Entry[] tab = table;
        //  得到当前 table的长度
                int len = tab.length;
        // 这个是对应的第一次添加数据的算索引方法   int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                int i = key.threadLocalHashCode & (len-1);
    //  这个和聊map的时候差不多了 只不过这个有算法得到了i直接查看所以为i内容  而且通过nextIndex可以看出这个是一个单链表如果不动可以看map源码讲解
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
                    // 如果算法后作为key有值那么就替换,然后return
                    if (k == key) {
                        e.value = value;
                        return;
                    }
      // 如果算法后作为key有值没有那就换,然后return
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
               // 如果在table[i] 没有内容就直接添加
                tab[i] = new Entry(key, value);
        // table中内容个数+1
                int sz = ++size;
        //cleanSomeSlots 清除脏数据  sz>大于 table长度*2/3 就进行扩容
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    //cleanSomeSlots 清除脏数据,也就是循环处理key=null的entry  但是不是全部而是一部分  然后然后删除
    private boolean cleanSomeSlots(int i, int n) {
                boolean removed = false;
                Entry[] tab = table;
                int len = tab.length;
                do {
                    i = nextIndex(i, len);
                    Entry e = tab[i];
                    if (e != null && e.get() == null) {
                        n = len;
                        removed = true;
                        i = expungeStaleEntry(i);
                    }
                } while ( (n >>>= 1) != 0);
                return removed;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    然后再看:

     private void rehash() {
         //前面说cleanSomeSlots是清除一部分脏数据,而这个是再次排除脏数据
                expungeStaleEntries();
    
                // expungeStaleEntries() 清洗数据会修改size  现在size值还大于 3/4 threshold就进行扩容
                if (size >= threshold - threshold / 4)
                    resize();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    // 前面说cleanSomeSlots是清除一部分脏数据,而这个是再次排除脏数据
    private void expungeStaleEntries() {
                Entry[] tab = table;
                int len = tab.length;
                for (int j = 0; j < len; j++) {
                    Entry e = tab[j];
                    if (e != null && e.get() == null)
                        expungeStaleEntry(j);
                }
            }
    
    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;
            }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    现在看一下扩容:

    private void resize() {
                Entry[] oldTab = table;
                int oldLen = oldTab.length;
        // 可见扩容的是2倍 具体这里面具体逻很像是map 所以就不再解读了
                int newLen = oldLen * 2;
                Entry[] newTab = new Entry[newLen];
                int count = 0;
    
                for (int j = 0; j < oldLen; ++j) {
                    Entry e = oldTab[j];
                    if (e != null) {
                        ThreadLocal<?> k = e.get();
                        if (k == null) {
                            e.value = null; // Help the GC
                        } else {
                            int h = k.threadLocalHashCode & (newLen - 1);
                            while (newTab[h] != null)
                                h = nextIndex(h, newLen);
                            newTab[h] = e;
                            count++;
                        }
                    }
                }
    
                setThreshold(newLen);
                size = count;
                table = newTab;
            }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    弱引用 Entry对象

    在前面聊四种引用的时候,就是为了再这里使用.如果不懂可以看一下前面的文章传送阵

    // 可以看出  Entry是 弱引用的子类
    static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    // 这里同key 就是 ThreadLocal  这个就是弱引用
                    // 而这个地方就是牛逼之处,如果我们使用若引起: new WeakReference(new ThreadLocal());
                    //这里用super调用父类构造方法代替new WeakReference,然后参数为k(ThreadLocal对象)
                    super(k);
                    value = v;
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    所以这个地方就是一个弱应用.

    可以这里可以看出在ThreadLocal中Entry其key为弱引起,而值为赋予的传递独立值.而这种弱引用是为了解决内存泄漏呢?下面开始聊;

    原理图

    在这里插入图片描述

    原理大概是这样要给图现在开始依次聊一下,看threadlocal为了解决内存泄漏做了哪些工作.

    • 首先声明一点,如果不适用threadlocal是否可以让线程有一个独立的参数内容?

      这个是一定可以的,可以为线程创建一个Map也能进行绑定,然后依次使用,或者创建一个私有属性也可以传递.但是为什么要创建threadlocal这个类的,.

      • 其实线程传递一个值,这个是有有需求
      • 既然需要这个功能那为什么不在底层代码写一个呢,让程序员编写的时候代码更加优雅呢,所以就诞生了这个类,而这个类不但可以方便传递数据,而且还尽量解决内存泄露的时期。
    • 为解决内存泄漏threadlocal类做了哪些努力呢?

      首先entry存储一个数据key-value的时候,key是threadlocal本身而且是弱引用,这个弱引用有很大的作用。

      • 如果不是弱引起threadlocal对应的变量设置为null的时候,threadlocal变量指定的存储位置是不会被回收的,毕竟key还是指定这个存储位置的,而这个弱引用当threadlocal为null,这个key是弱引用所以被释放掉,这个解决了一定的内存泄漏问题。
      • 但是要注意一件事,那就是value指向的值是强引用,而且key为null的话,其也可以指向value。所以数据如果不用的话,一定要调用threadlocal.remove(),所以把这个数据从value中删除(不然也会有value导致内存储泄漏)。
  • 相关阅读:
    《我在美国学游戏设计》笔记
    二叉排序树的删除操作的实现(思路分析)
    阿里大牛解析淘宝与Twitter 分布式系统案例与其架构设计原来源码
    java知识点快速过
    从零开始使用webpack搭建一个react项目
    java运算符
    Linux学习第35天:Linux LCD 驱动实验(二):星星之火可以燎原
    vue3源码分析——解密nextTick的实现
    JVM内存模型
    2023天津科技大学计算机考研信息汇总
  • 原文地址:https://blog.csdn.net/u011863822/article/details/126545935