美文网首页
ThreadLocal 分析

ThreadLocal 分析

作者: 伊泽瑞额 | 来源:发表于2021-07-21 17:30 被阅读0次

ThreadLocal 作用:实现数据隔离

ThreadLocal 原理分析:

Thread.java
public class Thread implements Runnable {
    ...
     ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}

每个线程Thread都维护了自己的threadLocals变量,ThreadLocalMap是由ThreadLocal维护的静态内部类,我们在使用ThreadLocal的get()、set()方法时,其实都是调用了ThreadLocalMap类对应的get()、set()方法。

set 源码:

 public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        
        //获取ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        
        if (map != null)//校验对象是否为空
          //如果已经存在,则通过ThreadLocalMap的set方法设置值,这里我们可以看到set中key为this,也就是当前ThreadLocal对象,而value值则是我们要存的值。
            map.set(this, value); 
        else
          //如果Thread中的对应属性为null,则创建一个ThreadLocalMap并赋值给Thread:
            createMap(t, value);// 为空创建一个map对象
            
}

hreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }


void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

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

上述set方法中,首先获取当前线程对象,然后通过getMap方法来获取当前线程中的threadLocals:
在get方法中可以看到同样通过当前线程,拿到当前线程的threadLocals属性,然后从中获取存储的值并返回。在get的时候,如果Thread中的threadLocals属性未进行初始化,则也会间接调用createMap方法进行初始化操作。



ThreadLocalMap是当前线程Thread一个叫threadLocals的变量中获取的。


public class Thread implements Runnable {
      ……
       ThreadLocal.ThreadLocalMap threadLocals = null;
       
       ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
        
       ……

1、Thread中的这个变量的初始化通常是在首次调用ThreadLocal的get()、set()方法时进行的。判断存不存在,不存在就调用createMap()来创建

2、thread 类有个 ThreadLocalMap 成员变量,这个Map key是Threadlocal 对象,value是你要存放的线程局部变量。每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。

ThreadLocal使用实例

ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();

线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。

使用场景

项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。

before

void work(User user) {
    getInfo(user);
    checkInfo(user);
    setSomeThing(user);
    log(user);
}

then

void work(User user) {
    try{
        threadLocalUser.set(user);
        // 他们内部  User u = threadLocalUser.get(); 就好了
        getInfo();
        checkInfo();
        setSomeThing();
        log();
        } finally {
         threadLocalUser.remove();
    }
}

ThreadLoalMap的数据结构

ThreadLoalMap是ThreadLocal中的一个静态内部类,类似HashMap的数据结构,但并没有实现Map接口。

ThreadLoalMap中初始化了一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对。通过上面的set方法,我们已经知道其中的key永远都是ThreadLocal对象。

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

    private static final int INITIAL_CAPACITY = 16;

    // ...
  构造方法
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
          table = new Entry[INITIAL_CAPACITY];
          int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
          table[i] = new Entry(firstKey, firstValue);
          size = 1;
          setThreshold(INITIAL_CAPACITY);
...
      }
}

上述构造方法,创建了一个默认长度为16的Entry数组,通过hashCode与length位运算确定索引值i。而上面也提到,每个Thread都有一个ThreadLocalMap类型的变量。

hash冲突及解决

我们留意到构造方法中Entry在table中存储位置是通过hashcode算法获得。每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647。

在向ThreadLocalMap中的Entry数值存储Entry对象时,会根据ThreadLocal对象的hash值,定位到table中的位置i。这里分三种情况:

  • 如果当前位置为空的,直接将Entry存放在对应位置;

  • 如果位置i已经有值且这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;

  • 如果位置i的Entry对象和即将设置的key没关系,则寻找一个空位置;

上面的流程可以看出这里采用的是开放地址方法,如果当前位置有值,就继续寻找下一个位置,注意table[len-1]的下一个位置是table[0],就像是一个环形数组,所以也叫闭散列法。如果一直都找不到空位置就会出现死循环,发生内存溢出。当然有扩容机制,一般不会找不到空位置的。

ThreadLocal内存泄露

根据前面对ThreadLocal的分析,得知每个Thread维护一个ThreadLocalMap,它key是ThreadLocal实例本身,value是业务需要存储的Object。也就是说ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

仔细观察ThreadLocalMap,这个map是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC时会被回收。

正常来说,当Thread执行完会被销毁,Thread.threadLocals指向的ThreadLocalMap实例也随之变为垃圾,它里面存放的Entity也会被回收。这种情况是不会发生内存泄漏的。

发生内存泄露的场景一般存在于线程池的情况下。此时,Thread生命周期比较长(存在循环使用),threadLocals引用一直存在,当其存放的ThreadLocal被回收(弱引用生命周期比较短)后,对应的Entity就成了key为null的实例,但value值不会被回收。如果此Entity一直不被get()、set()、remove(),就一直不会被回收,也就发生了内存泄漏。

所以,通常在使用完ThreadLocal后需要调用remove()方法进行内存的清除。

为什么使用弱引用而不是强引用?

我们先来假设一下,如果key使用强引用,那么在其他持有ThreadLocal引用的对象都回收了,但ThreadLocalMap依旧持有ThreadLocal的强引用,这就导致ThreadLocal不会被回收,从而导致Entry内存泄露。

对照一下,弱引用的情况。持有ThreadLocal引用的对象都回收了,ThreadLocalMap持有的是ThreadLocal的弱引用,会被自动回收。只不过对应的value值,需要在下次调用set/get/remove方法时会被清除。

综合对比会发现,采用弱引用反而多了一层保障,ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。

所以,内存泄露的根本原因是是否手动清除操作,而不是弱引用。

相关文章

网友评论

      本文标题:ThreadLocal 分析

      本文链接:https://www.haomeiwen.com/subject/hlbjmltx.html