老弟带你看ThreadLocal

老弟带你看ThreadLocal

Scroll Down

ThreadLocal

ThreadLocal类用于保存线程局部变量。这些变量与普通变量的不同之处在于,每个访问变量的线程(通过get()set()方法)都有自己独立初始化的变量副本。ThreadLocal类的实例通常是类中的私有静态字段。所存放的变量一般是会和当前线程所绑定,(例如,登陆用户的用户ID或事务状态)。

类设计

WX20201225-114259
看类图,ThreadLocal只有一个静态内部类ThreadLocalMap,SuppliedThreadLocal是在1.8增加的。属实本地线程隔离全靠ThreadLocalMap来实现。

ThreadLocalMap

该类是包私有的,ThreadLocalMap是一个自定义哈希表,用于维护线程本地变量。在ThreadLocal类之外无法进行任何访问。同时Thread中类中有ThreadLocal.ThreadLocalMap字段的声明,用于父子线程继承局部变量,哈希表entry使用弱引用,是为了处理非常大且长期存在。对于不调用remove方法时候。只有当表开始用完空间时,旧的数据的条目才会被删除设置为null。在gc时候被回收。(即entry.get()== null)意味着键不再被引用,才可以从表中删除。

SuppliedThreadLocal

继承了ThreadLocal,内部组合函数接口Supplier,用于设定初始值,使用构造传入。

源码分析

public class ThreadLocalDemo {

    private static final ThreadLocal<Map<String,String>> threadLocal =ThreadLocal.withInitial(()->new HashMap<String,String>(){{put("init","我最美");}});

    public static void main(String[] args){
        CountDownLatch countDownLatch=new CountDownLatch(1);
        new Thread(()->{
            System.out.println(threadLocal.get());
            threadLocal.set(new HashMap<String,String>(){{put("subThread","bb");}});
            System.out.println(threadLocal.get());
            countDownLatch.countDown();

        }).start();
        try {
            countDownLatch.await();
            System.out.println(threadLocal.get());
            threadLocal.set(new HashMap<String,String>(){{put("main","bb");}});
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(threadLocal.get());
    }
}
output:

{init=我最美}
{subThread=bb}
{init=我最美}
{main=bb}
Process finished with exit code 0

一个简简单单的代码中可以看到ThreadLocal和SuppliedThreadLocal均无法在父子线程中实现变量的继承,但是实现了变量的线程隔离。废话,人家本来就是干这事需要你说。啧啧啧
构造方法未使用默认无参构造使用ThreadLocal.withInitial是为了演示SuppliedThreadLocal的使用方式,可以实现某些场景如getOrNotExistDefault

设置变量

创建ThreadLocal对象之后,并未对ThreadlocalMap进行初始化。在对其进行写入时会,进行调用createMap来初始化ThreadlocalMap初始化是则是初始化Thread内部ThreadLocal.ThreadLocalMap字段,是不是明白怎么实现线程隔离了。那个线程的内部变量,可不就是私有,西八。调用构造方法创建entry数组。注意此entry对象是弱引用。当在GC线程扫描它所管辖的内存区域的过程中,一旦发现了只有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。这样在Entry中key会被回收 但是value不会被回收,这就是造成内存泄漏。

  public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
   void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

读取变量

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变量的同时如果发现当前的key获取不到,判断this.referent是否为null,如果为空会设置当前value为null,设置当前的entry的数组下标对象为空,更新容量,方便在下一次GC的时候回收。如果有默认值在取不到值得情况下返回默认值 反之返回null;

回收变量,删除变量

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

删除变量的同时会从更具当前的线程获取到当前线程的ThreadLocal.ThreadLocalMap字段从其中读取key,当命中之后。会设置this.referent是否为null,设置key为null设置value为null设置中,entry对应的数组下标值为null方便GC。

最佳实践

使用ThreadLocal要避免在线程池当中使用,这样变量极容易泄漏。还有使用之时设置变量,在使用完毕之后需要手动调用remove方法来删除。

延伸阅读InheritableThreadLocal

InheritableThreadLocal类提供值的继承从父线程到子线程:在创建子线程时,子接收所有可继承线程局部变量的初始值父线程的值。 一般来说,子线程的值将是与父线程相同;但子线程的值可以通过重写childCValue方法来更改值。


public class ThreadLocalDemo {

    private static final InheritableThreadLocal<Map<String,String>> threadLocal =new InheritableThreadLocal<>();

    public static void main(String[] args){
        threadLocal.set(new HashMap<String,String>(){{put("mailThread","bb");}});
        CountDownLatch countDownLatch=new CountDownLatch(2);
        new Thread(()->{
                countDownLatch.countDown();
                System.out.println(threadLocal.get());
                Map<String,String> t=threadLocal.get();
                t.put("SubThread1","aaa");
                threadLocal.set(t);
        }).start();
        new Thread(()->{
            countDownLatch.countDown();
            System.out.println(threadLocal.get());
            Map<String,String> t=threadLocal.get();
            t.put("SubThread2","aaa");
            threadLocal.set(t);
        }).start();
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(threadLocal.get());
    }
}
output
{mailThread=bb}
{mailThread=bb, SubThread1=aaa}
{mailThread=bb, SubThread1=aaa, SubThread2=aaa}
Process finished with exit code 0

总结

ThreadLocal提供了本地线程私有变量,主要用于线程隔离,互不影响,相当于每个线程均保留一份副本,另一层面的解决了竞态访问。但是由于WeakReference的key导致了在没有强引用应用情况下,会被GC回收,但是对应的value值却无法回首,然而ThreadLocal类只有对其进行访问或者容量满了之后才回去删除过期的Entry,这就容易造成内存泄漏。用完之后remove的习惯要写在回忆里。还有当需要在父子线程传输变量可以使用InheritableThreadLocal.