java面试系列(3)—— ThreadLocal

说一下ThreadLocal

1.ThreadLocal 是java中所提供的线程本地存储机制,可以利用该机制将数据(如对象)缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
2.ThreadLocal底层是通过ThreadLocalMap实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
3.如果线程池中使用ThreadLocal会造成内存泄漏,因为当使用完ThreadLocal对象后,理应当把设置的key、value,也就是Entry对象进行回收,但是线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏。解决方法,在一个线程使用完ThreadLocal后,手动调用ThreadLocal对象的remove方法移除Entry对象。
4.ThreadLocal经典应用场景是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享一个连接)

一.ThreadLocal使用案例

//定义一个Person类,其中的成原变量使用了ThreadLocal
public class Person {
    ThreadLocal<String> name=new ThreadLocal<>();

    public String getName() {
        return name.get();
    }

    public void setName(String name) {
        this.name.set(name);;
    }

    public Person() {
    }
}

//测试方法,创建了两个线程,每个线程分别等待3秒,并对Person中的成员变量进行赋值
public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        new Thread(()->{
            try{
                Thread.sleep(3000);
            }catch (Exception e){
                System.out.println(e);
            }
            person.setName("一号");
            System.out.println("1线程:"+person.getName());
        }).start();


        new Thread(()->{
            try{
                Thread.sleep(3000);
            }catch (Exception e){
                System.out.println(e);
            }
            person.setName("二号");
            System.out.println("2线程:"+person.getName());
        }).start();
    }
}

100次测试结果都为:

1线程:一号
2线程:二号

主要原因是就是ThreadLocal:ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,防止自己的变量被其它线程篡改。
底层其实就是当我们在一个公共类Person中声明了一个ThreadLocal对象后,当一个线程使用该person并使用set进行赋值时,会将 <ThreadLocal,对应的数据> 封装成一个Entry对象,并将其放入每个线程独有的ThreadThreadMap中。
我们看看在Thread类中这个Map是哪个成员变量:
java面试系列(3)—— ThreadLocal
结构如下:
java面试系列(3)—— ThreadLocal

当一个线程创建时,就会初始化这个threadLocals,当我们调用set方法时,底层代码如下;

//class ThreadLocal
 public void set(T value) {
        Thread t = Thread.currentThread();   //当前线程
        ThreadLocalMap map = getMap(t);   //找到当前线程对应的threadLocals
        if (map != null)
            map.set(this, value); // map在线程开始时就已经创建好了,我们这里直接将ThreadLocal和value装入map中
        else
            createMap(t, value);
    }

调用get方法时,源码如下:

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();
   }

从上面可知,每个线程执行的get和set都是执行线程本地缓存的数据,从而使线程之间非共享数据得到了隔离。

二.内存泄漏问题

内存泄漏:不再使用的对象没有被垃圾回收,导致jvm的内存压力逐渐增大

当一个线程结束后,线程相关的对象如ThreadLocalMap中的Entry会被垃圾回收。但是在线程池中,一个线程不会被销毁,他会做不同的逻辑任务,Entry对象不会被垃圾回收,但是之前的Entry对象我们不会再使用,这样就耗费了jvm的内存,即内存泄漏问题。解决方法,在公共对象中提供remove方法

public void remove(){
        this.name.remove();
    }

这样在一个线程使用完一个ThreadLocal属性后,手动调用remove方法,将本线程中ThreadLocalMap中对应ThreadLocal的key进行删除即可,这样就避免了内存泄漏问题。

上一篇:Redis 源码简洁剖析 06 - quicklist 和 listpack


下一篇:Solana 101