ThreadLocal底层原理


参考蚂蚁课堂

1.什么是ThreadLocal?

ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定。ThreadLocal适用于在多线程情况下,可以实现传递数据,实现线程隔离。

2.ThreadLocal基本用法

可以创建一个TheadLocal的对象然后set是设置内容,get是获取内容。可以做到线程之间变量的隔离。

    public static void main(String[] args) {
        ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
        stringThreadLocal.set("wjz nb");
        new Thread(() -> {
            stringThreadLocal.set("wjz mvp");
            System.out.println("子线程:" + stringThreadLocal.get());
        }).start();
        System.out.println("主线程:"+stringThreadLocal.get());
    }

我们可以开一个主线程在主线程中将ThreadLocal设置为"wjz nb",然后再开一个子线程在子线程中设置stringThreadLocal值为"wjz mvp",然后我们执行这段程序。看看结果如果主线程和子线程的值不同说明ThreadLocal确实隔离了,如果他们两个的值相同说明没隔离。

在这里插入图片描述

结果显示他俩确实是隔离的。

3.ThreadLocal的应用场景

  • 设计模式中的模板方法,他会把一些公有的东西放在模板类类,其他方法交给子类来实现,那么就可以把一些变量缓存在一个模板类当中这样我的子类就可以去获取。

  • SpringMVC获取HttpRequest,首先tomcat接受请求,创建一个线程然后通过Servlet处理请求,SpringMVC对Servlet进行了一层封装,封装了HttpRequest对象放入当前的ThreadLocal然后再通过Aop拦截请求,然后在控制器层的ThreadLocal获取到该HttpRequest。虽然经过了重重处理,但是最终还是在同一个线程里。

    在这里插入图片描述

    比如说下面这段代码我想要获取HttpServletRequest对象,是通过RequestContextHolder获取的我们来看一下这个类里面是啥样的

    在这里插入图片描述

    我们可以看到这里面定义了ThreadLocal专门缓存HttpServletRequest对象,然后我们就可以在控制层通过ThreadLocal获取该线程的HttpServletRequest对象

  • AOP拦截请求,在Aop层缓存一个变量到ThreadLocal中,然后我们可以在控制层拿到这个变量。

4.ThreadLocal底层原理(点击参考ThreadLocal详细源码分析

TheadLocal是通过ThreadLocalMap存放的,ThreadLocalMap中可以存放多个不同的ThreadLocal对象,同一个线程可以通过ThreadLocalMap存放多个ThreadLocal对象。每个ThreadLocal对象只能缓存一个变量值。底层的ThreadLocalMap<ThreadLocal,Object>我们在执行ThreadLocal.get()实际上调用的是threadLocalMap.get(threadLocal),通过这个返回value值。我们看一下源码

    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,然后通过这个map.getEntry里面传入this即当前的ThreadLocal我们能获取到value值。

5.强软弱引用之间的区别

5.1强引用

当内存不足时,JVM开始进行GC,对于强引用对象,就算是出现了OOM也不会对该对象进行回收,死都不会回收。

在这里插入图片描述

如图所示就算堆内存不够了,抛异常也不会回收,这就是强引用。

5.2软引用

当系统内存充足的时候,不会被回收,当系统内存不足时,它会被回收,比如高速缓存就用到过软引用,内存够用时就保留,不够时就回收。下面我们来看一个例子

    public static void main(String[] args) {
        Object o1 = new Object();
        SoftReference<Object> softReference = new SoftReference<>(o1);
        System.out.println("o1:" + o1);
        System.out.println("软引用对象:" + softReference.get());
        o1 = null;
        try {
            byte[] bits = new byte[30 * 1024 * 1024];
        } catch (Throwable ex) {
        } finally {
            System.out.println("------------------------------------------");
            System.out.println("o1:" + o1);
            System.out.println("软引用对象:" + softReference.get());
        }
    }

首先我创建一个强引用对象o1,然后再把它设置成软引用,然后将o1置为null,打印o1被置为null前后的对象情况。

在这里插入图片描述

这是在内存充足的情况下,在o1不为null的情况下,o1强引用对象和软引用对象是一样的,o1被置为null之后,程序结束之前执行finally里面的代码,o1已经是null,所以o1为null,但是软引用没有被回收。

当我们设置一下JVM的参数,然后我把当前堆内存最大空间为5M,

在这里插入图片描述

然后突然申请bits一共30M,那肯定不够了,然后再运行看一下结果

在这里插入图片描述

我们可以看到当内存不足时软引用也被回收了。

5.3弱引用

public class WeakReferenceDemo {
    public static void main(String[] args) {
        Object o1 = new Object();
        WeakReference<Object> weakReference = new WeakReference<>(o1);
        System.out.println(o1);
        System.out.println(weakReference.get());
        o1 = null ;
        System.gc();
        System.out.println("===============");
        System.out.println(o1);
        System.out.println(weakReference.get());
    }
}

在这里插入图片描述

弱引用就是这样,内存空间足够的情况下只要发生GC他也会被回收。

5.4虚引用

虚引用对象,在收集器确定其引用对象可能被回收后排队。虚引用最常用于以比Java终结机制更灵活的方式安排死前清理操作,如果垃圾收集器在某个时间点确定虚引用的对象是可达的,则在该时间或稍后某个时间,它将使引用入队。他的构造器得参数就有这样的一个队列。

    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

虚引用的get方法始终返回空。所以一般情况下虚引用没啥大用。为对象生成虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。jdk中的直接内存回收就用到了虚引用,因为直接内存不在jvm管理范围,所以会在堆内存中分配一个对象保存这个堆外内存的引用,这个就是虚引用,一旦这个对象被回收,相应的用户线程就会收到通知并对直接内存进行清理工作。

6.ThreadLocal内存泄漏问题

内存泄漏就是我们申请了内存,但是该内存一直不会被释放。内存溢出就是申请内存,但是实际的内存不足,就是你管你妈要钱,但是你妈没钱。然后我们通过一段小demo描述一下ThreadLocal内存泄漏的问题。

    public static void main(String[] args) {
        ThreadLocal<String> stringThreadLocal1 = new ThreadLocal<>();
        stringThreadLocal1.set("wjz");
        stringThreadLocal1 = null;
        Thread thread = Thread.currentThread();
        System.out.println(thread);
    }

我们先为这个stringThreadLocal设置一个值,然后再把这个置为null,然后打断点看看这个线程里还有没有这个ThreadLocal。

在这里插入图片描述

我们可以来分析一下原因

在这里插入图片描述

如图所示每个线程中有自己独立的ThreadLocalMap,ThreadLocalMap底层基于Entry对象封装,Entry key=ThreadLocal堆内存空间内存地址指向,Entry key=使用弱引用的方式指向ThreadLocal堆内存地址 如果没有发生GC回收的情况下该对象不会被清除,即便发生gc,该ThreadLocal对象被清除,但是Entry对象也不会被清除,此时Entry对象中的key=null,value还会在。

7.如何防止ThreadLocal内存泄漏问题

1.ThreadLocal已经为我们封装好了一个remove方法,这个方法可以直接移除ThreadLocalMap中Entry数组中的Entry对象

    public static void main(String[] args) {
        ThreadLocal<String> stringThreadLocal1 = new ThreadLocal<>();
        stringThreadLocal1.set("wjz");
        // 移除ThreadLocalMap中Entry数组中的Entry对象
        stringThreadLocal1.remove();
        //stringThreadLocal1与堆内存中的ThreadLocal断开引用
        stringThreadLocal1 = null;
        Thread thread = Thread.currentThread();
        System.out.println(thread);
    }

看一下结果

在这里插入图片描述

刚才10号位置存储了我们输入的字符串现在10号位置没了,说明内存泄漏的问题已经解决。然后我们再点进去看一下remove方法。

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

我们先拿到当前线程对应的ThreadLocalMap如果这个map不为空的话就删除this(ThreadLocal对象),就是切断ThreadLocalMap对堆内存的引用。

2.ThreadLocal在每次set执行的时候会判断一下key是不是为空如果为空他会替换“陈旧的”Entry。

        private void set(ThreadLocal<?> key, Object value) {
            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.
            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();
                if (k == key) {
                    e.value = value;
                    return;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

如果key为空他会走进这个方法replaceStaleEntry。

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

如果key不存在,他就会在旧的槽上添加一个新的entry。综上所述ThreadLocal对象在不用的时候一定要调用一下remove()方法。

posted @ 2023-01-09 22:56  DiligentCoder  阅读(257)  评论(0编辑  收藏  举报