代码改变世界

Java ThreadLocal使用、原理、问题分析

2020-07-30 17:44  宋海宾  阅读(250)  评论(0)    收藏  举报

1. 概述

     ThreadLocal叫做线程本地变量,也叫做线程本地存储。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLocal类主要有四个方法,分别是:

          1)ThreadLocal.get:用来获取ThreadLocal在当前线程中保存的变量副本

          2)ThreadLocal.set:用来设置ThreadLocal在当前线程中变量的副本

          3)ThreadLocal.remove:用来删除ThreadLocal在当前线程中变量的副本

         4)ThreadLocal.initialValue:是一个protected方法,一般是用来在使用时进行重写的。在调用get()方法时,如果ThreadLocal没有被当前线程赋值或当前线程刚调用remove方法,就返回此方法值。

 

2.用法

 

public class ThreadLocalMyDemo {

 

 

 

  public static ThreadLocal<String> threadLocalData = new ThreadLocal<String>() {

 

 

 

 

    @Override

 

 

 

 

    public String initialValue() {

 

 

 

 

      return "default";   //返回默认值

 

 

 

 

    }

 

 

 

 

  };

 

 

 

 

  public static void main(String[] args) {

 

 

 

 

    new Thread(new ThreadDemo("Thread A")).start();

 

 

 

 

    new Thread(new ThreadDemo("Thead B")).start();

 

 

 

 

  }

 

 

 

 

}

 

 

 

 

class ThreadDemo implements Runnable {

 

 

 

 

  private String value;

 

 

 

 

  public ThreadDemo(String value) {

 

 

 

 

    this.value = value;

 

 

 

 

  }

 

 

 

 

  public void run() {

 

 

 

 

    Thread t = Thread.currentThread();

 

 

 

 

    String value1 = ThreadLocalMyDemo.threadLocalData.get();   //获取到遍历

 

 

 

 

    System.out.println("threadName=" + t.getName() + ";最初value1=" + value1);

 

 

 

 

    ThreadLocalTest.threadLocalVar.set(value);   //设置值

 

 

 

 

    String value2 = ThreadLocalTest.threadLocalVar.get();

 

 

 

 

    System.out.println("threadName=" + t.getName() + ";设值后value2=" + value2);

 

 

 

 

    ThreadLocalTest.threadLocalVar.remove();   //删除值

 

 

 

 

    String value3 = ThreadLocalTest.threadLocalVar.get();   //获取值

 

 

 

 

    System.out.println("threadName=" + t.getName() + ";删除后value3=" + value3);

 

 

 

 

  }
}

 

 

3.原理

   每个线程中存在一个ThreadLocalMap 表,获取当前线程的 ThreadLocalMap入参是当前线程实例。

   例如:

java.lang.Thread t = java.lang.Thread.currentThread();
         ThreadLocal.ThreadLocalMap map = getMap(t);
   
    存储或者获取变量:

  if (map != null)

    map.set(this, value);//注意此时的“this”是指该thread-local变量, 不同的threadLocal变量作为索引
  else
    createMap(t, value);
 
         value = map.get(this)
 
        ThreadLocalMap的Entry继承了WeakReference, 使用ThreadLocal作为键值
 

ThreadLocal中hash冲突的解决方法:

    和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

   ThreadLocalMap解决Hash冲突的方法就是简单的步长加1,寻找下一个相邻的位置。

在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

  初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

  然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

 

4.内存泄漏

 

 ThreadLocal 本身并不存储值,它只是作为一个 key保存到ThreadLocalMap中,但是这里要注意的是它作为一个key用的是弱引用,因为没有强引用链,弱引用在GC的时候可能会被回收。这样就会在ThreadLocalMap中存在一些key为null的键值对(Entry)。因为key变成null了,我们是没法访问这些Entry的,但是这些Entry本身是不会被清除的,为什么呢?因为存在一条强引用链。即线程本身->ThreadLocalMap->Entry也就是说,恰恰我们在使用线程池的时候,线程使用完了是会放回到线程池循环使用的。由于ThreadLocalMap的生命周期和线程一样长,如果没有手动删除对应key就会导致这块内存即不会回收也无法访问,也就是内存泄漏。

   其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。但是这些举动不能保证内存就一定会回收,因为可能这条线程被放回到线程池里后再也没有使用,或者使用的时候没有调用其get(),set(),remove()方法。

  内存泄漏归根结底是由于ThreadLocalMap的生命周期跟Thread一样长

 如何避免内存泄漏

        调用ThreadLocal的get()、set()方法后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

      如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

      在不使用线程池的前提下,即使不调用remove方法,线程的"变量副本"也会被gc回收,即不会造成内存泄漏的情况。