ThreadLocal

概念:

  • ThreadLocal提供了线程本地变量,也就是如果创建一个ThreadLocal变量,那么访问的这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存的里面的变量,从而避免了线程安全问题。
  • 每个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
  • 扩容:使用开放寻址方法;

使用示例:

package com.example.demo;

/**
 * @author SHshuo
 * @data 2021/11/6--14:17
 * ThreadLocal提供了线程本地变量,也就是如果创建一个ThreadLocal变量,
 * 那么访问的这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,
 * 实际操作的是自己本地内存的里面的变量,从而避免了线程安全问题。
 *
 * 不具有继承性
 */
public class ThreadLocalTest {

//    创建ThreadLocal变量
    static ThreadLocal<String> localVariable = new ThreadLocal<>();

    public static void main(String[] args) {
//        创建两个线程、使用ThreadLocal
        new Thread(() -> {
            localVariable.set("Thread Of A local variable");
            System.out.println("Thread Of A  : " + localVariable.get());

//            移除本地内存保存的副本
            localVariable.remove();

//            获取的是本地内存保存的副本、而不是主内存
            System.out.println("Thread Of A remove : " + localVariable.get());
        }, "A").start();

        new Thread(() -> {
            localVariable.set("Thread Of B local variable");
            System.out.println("Thread Of B  : " + localVariable.get());

//            移除本地内存保存的副本
            localVariable.remove();
            System.out.println("Thread Of B remove : " + localVariable.get());
        }, "B").start();
    }

}

实现原理,简单来说:

  • 每个线程的本地变量不是存放在ThreadLocal的实例里面,而是存放在调用线程的threadLocals变量(也就是每一个线程的本地内存的变量)里面。
  • ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳
  • threadLocals是一个hashmap的结构,其中key就是当前ThreadLocal的实例对象引用,value是通过set方法传递的值


解决线程之间的值传递

ThreadLocal不具有继承性

因为子线程获取的自己本地的工作空间,通过给主线程设置值不会传递到子线程的工作空间。
package com.example.demo;

/**
 * @author SHshuo
 * @data 2021/11/6--14:33
 */
public class ThreadLocalTest {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("With inheritance");

        new Thread(() -> System.out.println("子线程: " + threadLocal.get())).start();

        System.out.println("main: " + threadLocal.get());
    }
}


父子线程进行值传递

InheritableThreadLocal具有继承性;
原理:通过将本地InheritableThreadLocals变量复制一份到子线程InheritableThreadLocals变量里实现继承。、
场景:
  • 子线程需要存放ThreadLocal变量中的用户登录信息
  • 中间件需要把统一的id追踪的整个调用链路记录下来。等等
package com.example.demo;

/**
 * @author SHshuo
 * @data 2021/11/6--14:33
 * 通过将本地变量复制一份到子线程变量里实现继承。获取仍然是本地内存的变量
 * inheritance继承性的意思、使用InheritableThreadLocal实现
 */
public class InheritableThreadLocalTest {
    public static void main(String[] args) {
        ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        inheritableThreadLocal.set("With inheritance");

        new Thread(() -> System.out.println("子线程: " + inheritableThreadLocal.get())).start();

        System.out.println("main: " + inheritableThreadLocal.get());
    }
} 

线程池中的线程进行值传递

使用:TransmittableThreadLocal是阿里开源的线程池异步上线文传递的一个组件,并不是JDK自带的。

ThreadLocal的常见方法的实现

set()
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
                        //找到对应map里面entry的位置
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
get()
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

总结:

无论set()、get()方法都会首先获取当前的Thread对象,根据thread获取当前线程的成员变量ThreadLocalMap。
从而进行判断如果map为空,就会创建key:ThreadLocal;value:设置的值。不为空,就找到对应entry(key:ThreadLocal; value:设置的值)。

由ThreadLocal引申出四种引用类型

前提介绍:

  • 强引用:是指创建一个对象并把这个对象赋值给一个引用变量(new出的对象、等于号赋值)、内存不足也不会回收;
  • 软引用:用于内存敏感高速缓存。内存足够不回收、内存不足就会回收;
  • 弱引用:一旦发现弱引用对象就会回收、无论内存是否足够;
  • 虚引用:相当于没有引用、主要用来跟踪对象被垃圾回收的活动
引用链、可达性分析;

内存泄漏:

  • 当B使用了A的对象后,即使A被置为null,但是GC仍然不会回收。因为B还持有对A的引用,而且还是强引用。
  • 所以既不能回收,也不能利用使用,这就会造成内存泄漏。
A a = new A();
B b = new B(a);
针对本例题解决方法:
  • 将b = null,显示的设置消除引用。就会回收这个对象。
  • B使用弱引用(weakReference b = new weakReference (a)

ThreadLocal中的key引发的内存泄漏

如果ThreadLocalMap的Entry中强引用了ThreadLocal实例,即使ThreadLocal instance = null仍然无法通过GC回收。因为如果没有显示的将引用赋值为null(将entry设置为null),ThreadLocalMap仍会持有ThreadLocal的强引用、是不会被回收的(还会有引用链通过可达性分析,ThreadLocal实例仍然是可达的。这就会造成内存泄漏。

解决:

通过ThreadLocalMap中的Entry继承WeakReference弱引用,super(k):将每一个entry的key都是弱引用。
一旦发现就会回收无论内存是否够用,也就避免了内存泄漏的问题;
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

ThreadLocal中的value引发的内存泄漏

从上面那段代码表示了强引用的发生(被赋值);
value = v;
所以当线程一直不销毁,通过线程池复用的时候,value对象通过可达性分析一直是可达的,所以不会被回收。

解决:

  • 通过使用remove(),扫描key为null的Entry,将里面的value显示的置为null,就可以正常被回收了。
  • 通过remove防止内存泄漏;
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
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;
                }
            }
        }

总结:

内存泄漏的解决办法,实质:避免强引用;
  • 使用弱引用;(解决ThreadLocalMap中的key内存泄漏
  • 通过显式的置为null消除强引用(解决ThreadLocalMap中的value内存泄漏

思维脑图



参考:


hshuo的面试之路 文章被收录于专栏

作者目标是找到一份Java后端方向的工作 此专栏用来记录从Bilibili、书本、其他优质博客上面学习的内容 用于巩固、总结内容 主要包含Docker、Dubbo、Java基础、JUC、Maven、MySQL、Redis、SpringBoot、SpringCloud、数据结构、杂文、算法、计算机网络、操作系统、设计模式等相关内容

全部评论

相关推荐

Noel_:中石油是这样的 哥们侥幸混进免笔试名单 一看给我吓尿了
点赞 评论 收藏
分享
贪食滴🐶:你说熟悉扣篮的底层原理,有过隔扣职业球员的实战经验吗
点赞 评论 收藏
分享
3 收藏 评论
分享
牛客网
牛客企业服务