深入分析ThreadLocal的内存泄漏问题

  老套路,先列举下关于ThreadLocal常见的疑问,希望可以通过这篇学习笔记来解决这几个问题:
  
  ThreadLocal是用来解决什么问题的?
  
  如何使用ThreadLocal?
  
  ThreadLocal的实现原理是什么?
  
  可否举几个实际项目中使用ThreadLocal的案例?
  
  基础知识
  
  ThreadLocal是线程局部变量,和普通变量的不同在于:每个线程持有这个变量的一个副本,可以独立修改(set方法)和访问(get方法)这个变量,并且线程之间不会发生冲突。
  
  类中定义的ThreadLocal实例一般会被private static修饰,这样可以让ThreadLocal实例的状态和Thread绑定在一起,业务上,一般用ThreadLocal包装一些业务ID(user ID或事务ID)——不同的线程使用的ID是不相同的。
  
  如何使用
  
  case1
  
  从某个角度来看,ThreadLocal为Java并发编程提供了额外的思路——避免并发,如果某个对象本身是非线程安全的,但是你想实现多线程同步访问的效果,例如SimpleDateFormat,你可以使用ThreadLocal变量。
  
  public class Foo
  
  {
  
  // SimpleDateFormat is not thread-safe, so give one to each thread
  
  private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
  
  @Override
  
  protected SimpleDateFormat initialValue()
  
  {
  
  return new SimpleDateFormat("yyyyMMdd HHmm");
  
  }
  
  };
  
  public String formatIt(Date date)
  
  {
  
  return formatter.get().format(date);
  
  }
  
  }
  
  注意,这里针对每个线程只需要初始化一次SimpleDateFormat对象,其实跟在自定义线程中定义一个SimpleDateFormat成员变量,并在线程初始化的时候new这个对象,效果是一样的,只是这样看起来代码更规整。
  
  case2
  
  之前在yunos做酷盘项目的数据迁移时,我们需要按照用户维度去加锁,每个线程在处理迁移之前,都需要先获取当前用户的锁,每个锁的key是带着用户信息的,因此也可以使用ThreadLocal变量实现:image.png
  
  case3
  
  下面这个例子,我们定义了一个MyRunnable对象,这个MyRunnable对象会被线程1和线程2使用,但是通过内部的ThreadLocal变量,每个线程访问到的整数都是自己单独的一份。
  
  package org.java.learn.concurrent.threadlocal;
  
  /**
  
  * @author duqi
  
  * @createTime 2018-12-29 23:25
  
  **/
  
  public class ThreadLocalExample {
  
  public static class MyRunnable implements Runnable {
  
  private ThreadLocal<Integer> threadLocal =
  
  new ThreadLocal<Integer>();
  
  @Override
  
  public void run() {
  
  threadLocal.set((int) (Math.random() * 100D));
  
  try {
  
  Thread.sleep(2000);
  
  } catch (InterruptedException e) {
  
  }
  
  System.out.println(threadLocal.get());
  
  }
  
  }
  
  public static void main(String[] args) throws InterruptedException {
  
  MyRunnable sharedRunnableInstance = new MyRunnable();
  
  Thread thread1 = new Thread(sharedRunnableInstance);
  
  Thread thread2 = new Thread(sharedRunnableInstance);
  
  thread1.start();
  
  thread2.start();
  
  thread1.join(www.jinmagjt.com); //wait for thread 1 to terminate
  
  thread2.join(); //wait for thread 2 to terminate
  
  }
  
  }
  
  ThreadLocal关键知识点
  
  源码分析
  
  ThreadLocal是如何被线程使用的?原理如下图所示:Thread引用和ThreadLocal引用都在栈上,Thread引用会引用一个ThreadLocalMap对象,这个map中的key是ThreadLocal对象(使用WeakReference包装),value是业务上变量的值。image.png
  
  首先看java.lang.Thread中的代码:
  
  public
  
  class Thread implements Runnable {
  
  //......其他源码
  
  /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
  
  ThreadLocal.ThreadLocalMap threadLocals = null;
  
  /*
  
  * InheritableThreadLocal values pertaining to this thread. This map is maintained by the InheritableThreadLocal class.
  
  */
  
  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  
  //......其他源码
  
  Thread中的threadLocals变量指向的是一个map,这个map就是ThreadLocal.ThreadLocalMap,里面存放的是跟当前线程绑定的ThreadLocal变量;inheritableThreadLocals的作用相同,里面也是存放的ThreadLocal变量,但是存放的是从当前线程的父线程继承过来的ThreadLocal变量。
  
  在看java.lang.ThreadLocal类,主要的成员和接口如下:image.png
  
  withInitial方法,Java 8以后用于初始化ThreadLocal的一种方法,在外部调用get()方法的时候,会通过Supplier确定变量的初始值;
  
  public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
  
  return new SuppliedThreadLocal<www.leyouzaixan.cn>(supplier);
  
  }
  
  get方法,获取当前线程的变量副本,如果当前线程还没有创建该变量的副本,则需要通过调用initialValue方法来设置初始值;get方法的源代码如下,首先通过当前线程获取当前线程对应的map,如果map不为空,则从map中取出对应的Entry,然后取出对应的值;如果map为空,则调用setInitialValue设置初始值;如果map不为空,当前ThreadLocal实例对应的Entry为空,则也需要设置初始值。
  
  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"www.tdcqpt.cn)
  
  T result = (T)e.value;
  
  return result;
  
  }
  
  }
  
  return setInitialValue();
  
  }
  
  set方法,跟get方法一样,先获取当前线程对应的map,如果map为空,则调用createMap创建map,否则将变量的值放入map——key为当前这个ThreadLocal对象,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);
  
  }
  
  remove方法,删除当前线程绑定的这个副本
  
  public void remove(www.zzxcscl.com) {
  
  ThreadLocalMap m = getMap(Thread.currentThread());
  
  if (m !=www.kunLunyuLegw.com null)
  
  m.remove(this);
  
  }
  
  数字0x61c88647,这个值是HASH_INCREMENT的值,普通的hashmap是使用链表来处理冲突的,但是ThreadLocalMap是使用线性探测法来处理冲突的,HASH_INCREMENT就是每次增加的步长,根据参考资料1所说,选择这个数字是为了让冲突概率最小。
  
  /**
  
  * The difference between successively generated hash codes - turns
  
  * implicit sequential thread-local IDs into near-optimally spread
  
  * multiplicative hash values for power-of-two-sized tables.
  
  */
  
  private static final int HASH_INCREMENT = 0x61c88647;
  
  父子进程数据共享
  
  InheritableThreadLocal主要用于子线程创建时,需要自动继承父线程的ThreadLocal变量,实现子线程访问父线程的threadlocal变量。InheritableThreadLocal继承了ThreadLocal,并重写了childValue、getMap、createMap三个方法。
  
  public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  
  /**
  
  * 创建线程的时候,如果需要继承且父线程中Thread-Local变量,则需要将父线程中的ThreadLocal变量一次拷贝过来。
  
  */
  
  protected T childValue(http://www.wanxinyulept.com/chaoyue T parentValue) {
  
  return parentValue;
  
  }
  
  /**
  
  * 由于重写了getMap,所以在操作InheritableThreadLocal变量的时候,将只操作Thread类中的inheritableThreadLocals变量,与threadLocals变量没有关系
  
  **/
  
  ThreadLocalMap getMap(Thread t) {
  
  return t.inheritableThreadLocals;
  
  }
  
  /**
  
  * 跟getMap类似,set或getInheritableThreadLocal变量的时候,将只操作Thread类中的inheritableThreadLocals变量
  
  */
  
  void createMap(Thread http://www.zzhehong.com/chaoyue/ , T firstValue) {
  
  t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
  
  }
  
  }
  
  关于childValue多说两句,拷贝是如何发生的?
  
  首先看Thread.init方法,
  
  private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
  
  //其他源码
  
  if (inheritThreadLocals && parent.inheritableThreadLocals != null)
  
  this.inheritableThreadLocals www.zbzxyL12.com=
  
  ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  
  /* Stash the specified stack size in case the VM cares */
  
  this.stackSize = stackSize;
  
  /* Set thread ID */
  
  tid = nextThreadID();
  
  }
  
  然后看ThreadLocal.createInheritedMap方法,最终会调用到newThreadLocalMap方法,这里InheritableThreadLocal对childValue做了重写,可以看出,这里确实是将父线程关联的ThreadLocalMap中的内容依次拷贝到子线程的ThreadLocalMap中了。
  
  private ThreadLocalMap(ThreadLocalMap parentMap) {
  
  Entry[] parentTable = parentMap.table;
  
  int len = parentTable.length;
  
  setThreshold(len);
  
  table = new Entry[len];
  
  for (int j = 0; j < len; j++) {
  
  Entry e = parentTable[j];
  
  if (e != null) {
  
  @SuppressWarnings("unchecked")
  
  ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
  
  if (key != null) {
  
  Object value = key.childValue(e.value);
  
  Entry c = new Entry(key, value);
  
  int h = key.threadLocalHashCode & (len - 1);
  
  while (table[h] != null)
  
  h = nextIndex(h, len);
  
  table[h] = c;
  
  size++;
  
  }
  
  }
  
  }
  
  }
  
  ThreadLocal对象何时被回收?
  
  ThreadLocalMap中的key是ThreadLocal对象,然后ThreadLocal对象时被WeakReference包装的,这样当没有强引用指向该ThreadLocal对象之后,或者说Map中的ThreadLocal对象被判定为弱引用可达时,就会在垃圾收集中被回收掉。看下Entry的定义:
  
  static class Entry extends WeakReference<ThreadLocal<?>> {
  
  /** The value associated with this ThreadLocal. */
  
  Object value;
  
  Entry(ThreadLocal<?> k, Object v) {
  
  super(k);
  
  value = v;
  
  }
  
  }
  
  ThreadLocal和线程池一起使用?
  
  ThreadLocal对象的生命周期跟线程的生命周期一样长,那么如果将ThreadLocal对象和线程池一起使用,就可能会遇到这种情况:一个线程的ThreadLocal对象会和其他线程的ThreadLocal对象串掉,一般不建议将两者一起使用。
  
  案例学习
  
  Dubbo中对ThreadLocal的使用
  
  我从Dubbo中找到了ThreadLocal的例子,它主要是用在请求缓存的场景,具体代码如下:
  
  @Activate(group = {Constants.CONSUMER, Constants.PROVIDER}, value = Constants.CACHE_KEY)
  
  public class CacheFilter implements Filter {
  
  private CacheFactory cacheFactory;
  
  public void setCacheFactory(CacheFactory cacheFactory) {
  
  this.cacheFactory = cacheFactory;
  
  }
  
  @Override
  
  public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
  
  if (cacheFactory != null && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.CACHE_KEY))) {
  
  Cache cache = cacheFactory.getCache(invoker.getUrl(), invocation);
  
  if (cache != null) {
  
  String key = StringUtils.toArgumentString(invocation.getArguments());
  
  Object value = cache.get(key);
  
  if (value != null) {
  
  if (value instanceof ValueWrapper) {
  
  return new RpcResult(((ValueWrapper)value).get());
  
  } else {
  
  return new RpcResult(value);
  
  }
  
  }
  
  Result result = invoker.invoke(invocation);
  
  if (!result.hasException()) {
  
  cache.put(key, new ValueWrapper(result.getValue()));
  
  }
  
  return result;
  
  }
  
  }
  
  return invoker.invoke(invocation);
  
  }
  
  可以看出,在RPC调用(invoke)的链路上,会先使用请求参数判断当前线程是否刚刚发起过同样参数的调用——这个调用会使用ThreadLocalCache保存起来。具体的看,ThreadLocalCache的实现如下:
  
  package org.apache.dubbo.cache.support.threadlocal;
  
  import org.apache.dubbo.cache.Cache;
  
  import org.apache.dubbo.common.URL;
  
  import java.util.HashMap;
  
  import java.util.Map;
  
  /**
  
  * ThreadLocalCache
  
  */
  
  public class ThreadLocalCache implements Cache {
  
  //ThreadLocal里存放的是参数到结果的映射
  
  private final ThreadLocal<Map<Object, Object>> store;
  
  public ThreadLocalCache(URL url) {
  
  this.store = new ThreadLocal<Map<Object, Object>>() {
  
  @Override
  
  protected Map<Object, Object> initialValue() {
  
  return new HashMap<Object, Object>();
  
  }
  
  };
  
  }
  
  @Override
  
  public void put(Object key, Object value) {
  
  store.get().put(key, value);
  
  }
  
  @Override
  
  public Object get(Object key) {
  
  return store.get().get(key);
  
  }
  
  }
  
  RocketMQ
  
  在RocketMQ中,我也找到了ThreadLocal的身影,它是用在消息发送的场景,MQClientAPIImpl是RMQ中负责将消息发送到服务端的实现,其中有一个步骤需要选择一个具体的队列,选择具体的队列的时候,不同的线程有自己负责的index值,这里使用了ThreadLocal的机制,可以看下ThreadLocalIndex的实现:
  
  package org.apache.rocketmq.client.common;
  
  import java.util.Random;
  
  public class ThreadLocalIndex {
  
  private final ThreadLocal<Integer> threadLocalIndex = new ThreadLocal<Integer>();
  
  private final Random random = new Random();
  
  public int getAndIncrement() {
  
  Integer index = this.threadLocalIndex.get();
  
  if (null == index) {
  
  index = Math.abs(random.nextInt());
  
  if (index < 0)
  
  index = 0;
  
  this.threadLocalIndex.set(index);
  
  }
  
  index = Math.abs(index + 1);
  
  if (index < 0)
  
  index = 0;
  
  this.threadLocalIndex.set(index);
  
  return index;
  
  }
  
  @Override
  
  public String toString() {
  
  return "ThreadLocalIndex{" +
  
  "threadLocalIndex=" + threadLocalIndex.get() +
  
  '}';
  
  }
  
  }
  
  总结
  
  这篇文章主要是解决了关于ThreadLocal的几个问题:(1)具体的概念是啥?(2)在Java开发中的什么场景下使用?(3)ThreadLocal的实现原理是怎样的?(4)开源项目中有哪些案例可以参考?不知道你是否对这几个问题有了一定的了解呢?如果还有疑问,欢迎交流。
  
  参考资料
  
  Why 0x61c88647?
  
  Java ThreadLocal
  
  When and how should I use a ThreadLocal variable?
  
  技术小黑屋:理解Java中的ThreadLocal
  
  深入分析ThreadLocal的内存泄漏问题
  
  《Java并发编程实战》
  
  InheritableThreadLocal详解
  
  ThreadLocal详解
  
  ThreadLocal的使用场景
  
  数据结构:哈希表

猜你喜欢

转载自www.cnblogs.com/qwangxiao/p/11222917.html