Java ThreadLocal 有内存泄漏的风险怎么搞?分析下原理吧

通过阅读本遍你将获取的知识

  1. ThreadLocal 使用方法
  2. ThreadLocal 适合使用的场景
  3. ThreadLocal实现方法与原理
  4. ThreadLocalMap实现方法与原理
  5. Thread如何存储ThreadLocalMap
  6. ThreadLocalMap内存泄漏原因与避坑方法

ThreadLocal被解释为线程本地变量,生命周期与和它绑定的线程相同,在新创建一个线程时会初始化一个ThreadLocal实例,线程销毁时与会被一同销毁,在线程内部只能访问到本线程自己的ThreadLocal实例变量。

下面通过一个例子来理解ThreadLocal

public static  class ConnectionManger{
  private static Connection connection;

  public static Connection openConnection() throws SQLException {
    if(connection==null){
      connection=DriverManager.getConnection("");
    }
    return connection;
  }

  public static void close() throws SQLException {
    if(connection!=null){
      connection.close();
      connection=null;
    }
  }
}
public void demo() throws SQLException {
  Connection connection = ConnectionManger.openConnection();
  connection.close();
}

ConnectionManger 类为一个管理数据库连接的类,在内部有一个共享变量connection
单线程的应用环境下调用demo方法不会出现什么问题,运行也正常。
多线程环境下就不好使了,调用demo方法的结果将会不可控,可能什么出现多个线程使用现一个Connection对象,一个关闭连接后其它的出现异常。
解决多线程中的问题可以在ConnectionManager的方法中添加同步块,通过同步块保证多线程环境下的线程安全,但ConnectionManager将会成为多线程环境下访问数据库能力上的瓶颈,所有的数据读写都将串行化执行。

解决性能瓶颈的办法是将ConnectionManager也多线程化,将它做为一个连接存储池(数据库连接池),通过动态配置池的大小来控制数据读写能力。那如何解决业务上多线程使用Connection实例的问题呢?

ThreadLocal

主角登场,通过上面问题的分析,每一个业务线程在需要使用数据库连接时,通过调用**ConnectionManger.openConnection()方法获取一个连接,在业务线程中通过connection实例完成业务操作,再通过ConnectionManger.close()**关闭连接;业务之前是相互隔离的,每一个业务分配一个connection就可以解决串行化的问题。

每个线程都有一个ThreadLocal实例,将共享的connection实例存储在ThreadLocal,在线程中执行的业务只需要获取ThreadLocal中的connection实例完成数据读写即可,下面看下优化后的ConnectionManager对象:

public void demo() throws SQLException {
  Connection connection = ConnectionManger.openConnection();
  connection.close();
}
public static class ConnectionManger {
  static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
  //模拟连接池获取连接
  private static Connection openNewConnection() {
    try {
      return DriverManager.getConnection("");
    } catch (SQLException ignore) {
    }
    return null;
  }
  public static Connection openConnection() throws SQLException {
    threadLocal.set(ConnectionManger.openNewConnection());
    return threadLocal.get();
  }
  public static void close() throws SQLException {
    threadLocal.get().close();
    threadLocal.remove();
  }
}

在ConnectionManager对象中引用ThreadLocal<Connection>变量类型存储当前线程中使用的Connection实例。

通过优化解决了多线程中线程对资源竞争的问题,通过添加同步块导致的性能问题。

你好奇ThreadLocal怎么实现的吗?我好奇

ThreadLocal实现

ThreadLocal实例供外部使用的三个方法分别是:

ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
threadLocal.get();//获取值
threadLocal.set(<Object>);//设置值
threadLocal.remove();//移除值

get()用于获取存储在当前线程中值,set()用于将值存储在当前线程中,remove()用于移除当前线程中的值。

下面来简单过下各个方法是怎么实现的:

public T get() {
  //获取当前线程
  Thread t = Thread.currentThread();
  //获取线程中的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
    //获取Map中的值
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
      @SuppressWarnings("unchecked")
      T result = (T)e.value;
      return result;
    }
  }
  //没有map时进行初始化
  return setInitialValue();
}
public void set(T value) {
  //获取当前线程
  Thread t = Thread.currentThread();
  //获取线程中的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
    //将值存储在Map
    map.set(this, value);
  } else {
    //新创建一个Map
    createMap(t, value);
  }
}
public void remove() {
  //获取当前线程的Map
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null) {
    //将值从Map中移除
    m.remove(this);
  }
}
//获取线程的Map
ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
}

从上面的源码中可以得到几个关键知识点:

  1. Thread使用ThreadLocalMap管理引用的值,ThreadLocalMap是ThreadLocal内部类
  2. ThreadLocalMap实例存储在线程的threadLocals变量中
  3. ThreadLocal的set get remove实例操作的是当前线程中存储在threadLocals变量中的值
  4. Thread中的threadLocals可能为null,为null时get set 时会进行初始化

ThreadLocalMap

要完全搞清楚ThreadLocal的底层原理,ThreadLocalMap的实现肯定是要搞清楚的。
在内部和Map的思路比较像,但存储池改为了数组,Entry对象继承自WeakReference弱引用类型,和Map类型相似点在于有扩容操作,ThreadLocalMap不继承于Map类型,只在思想上相同。

可能有小伙伴要问了,为什么不直接用Map类型来存储呢,在存储数据量比较多时查找性能优于数组;这里解释下没有用Map类型的原因于一般每个线程中存储在ThreadLocalMap中的数据不会特别多,在这样的情况下使用数组的性能就要优于Map类型了,查找时不用计算Hash值,数据量大于扩容也比较简单。

ThreadLocalMap内存泄漏

ThreadLocalMap使用内部Entry做为存储数据的结构,先来看下实现代码:

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

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

在Entry内部将ThreadLocal实例做为Key,将值做为value变量,ThreadLocalMap的存储池中将直接存储Entry实例,从Entry类的结构上可以看出继承了WeakReference类型,在构建方法中对TheadLocal实例的引用值调用 super(k),这样做的结果是Entry类型中的key对ThreadLocal的值引用为弱引用类型;
在JVM的GC回收中,如果一个值只是被弱引用了,那么将在GC回收时对这个引用进行回收。

这又怎样能导致内存泄漏呢?

根据弱引用的特性,一个值只被弱引用时会被GC回收,那一起来分析下下面的示例代码:

public static class ConnectionManger {
  private static Connection openNewConnection() {
    try {
      return DriverManager.getConnection("");
    } catch (SQLException ignore) {
    }
    return null;
  }
  public static Connection openConnection() throws SQLException {
    ThreadLocal<Connection> local = ThreadLocal.withInitial(ConnectionManger::openNewConnection);
    return local.get();
  }
}

设上面的代码运行在多线程环境,且线程使用线程池管理,线程池没有停步且在任务队列中还有等待执行的任务。

openConnection中使用ThreadLocal.withInitial创建了一个ThreadLocal实例,随后使用get方法返回值。

在这个示例的openConnection方法中没有对ThreadLocal实例做强引用,在方法执行完成后局部变量将会被回收,随后也没有调用ThreadLocal的实例方法remove将引用的值移除掉。

现在我们来分析下内存中JVM进行GC后的情况:

  1. 使用ThreadLocal.withInitial创建TheadLocal实例后再使用get方法获取时会将初始值存入线程对应的ThreadLocalMap中,在TheadLocalMap中会使用Entry类型将ThreadLocal类型值做为key,将Connection类型值做为value
  2. JVM开始执行GC对内存进行回收,回收时发现Key是弱引用,在其它地址没有再引用(没有强引用),JVM对Key的值进行回收

在JVM执行完GC后key被成功回收,但value引用的值并没有被回收,这时ThreadLocalMap中的Entry的Key将变为null,而value则没有变,因为没有执行remove操作,这条记录将会一直存在于ThreadLocalMap中直到线程被销毁后导致线程引用的ThreadLocalMap实例被回收才会将泄漏的内存进行回收。

精彩推荐:
包邮赠书!《阿里巴巴Java开发手册 第二版》
知识点:Java sychronized 内部锁实现原理
知识点: Java FutureTask 使用详解
Java ClassLoader详解双亲委派的实现原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值