本地变量ThreadLocal怎么用?为什么会发生内存泄漏?

简介 


ThreadLocal是 Java 中提供的一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离,用于解决多线程中共享对象的线程安全问题。

通常,我们会使用synchronzed关键字或者lock来控制线程对临界区资源的同步顺序,但这种加锁的方式会让未获取到锁的线程进行阻塞,很显然,这种方式的时间效率不会特别高。

线程安全问题的核心在于多个线程会对同一个临界区的共享资源进行访问,那如果每个线程都拥有自己的“共享资源”,各用各的,互不影响,这样就不会出现线程安全的问题了,对吧?

事实上,这就是一种“空间换时间”的思想,每个线程拥有自己的“共享资源”,虽然内存占用变大了,但由于不需要同步,也就减少了线程可能存在的阻塞问题,从而提高时间上的效率。

不过,ThreadLocal 并不在 java.util.concurrent 并发包下,而是在 java.lang 包下,但我更倾向于把它当作是一种并发容器。

ThreadLocal 就是线程的“本地变量”,即每个线程都拥有该变量的一个副本,达到人手一份的目的,这样就可以避免共享资源的竞争

ThreadLocal源码分析


set方法

set 方法用于设置当前线程中 ThreadLocal 的变量值,该方法的源码如下:

public void set(T value) {
	//1. 获取当前线程实例对象
    Thread t = Thread.currentThread();

	//2. 通过当前线程实例获取到ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);

    if (map != null)
	   //3. 如果Map不为null,则以当前ThreadLocal实例为key,值为value进行存入
       map.set(this, value);
    else
	  //4.map为null,则新建ThreadLocalMap并存入value
      createMap(t, value);
}
  • 通过 Thread.currentThread() 方法获取当前调用此方法的线程实例。
  • 每个线程都有自己的 ThreadLocalMap,这个映射表存储了线程的局部变量,其中键是 ThreadLocal 对象,值为特定于线程的对象。
  • 如果 Map 不为 null,则以当前 ThreadLocal 实例为 key,值为 value 进行存入;如果 map 为 null,则新建 ThreadLocalMap 并存入 value。

通过源码我们知道,value 是存放在 ThreadLocalMap 里的。

ThreadLocalMap 是怎样来的呢

ThreadLocalMap getMap(Thread t) {
    return t.ThreadLocals;
}

再来看 set 方法,当 map 为 null 的时候会通过createMap(t,value)方法 new 出来一个:

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

该方法 new 了一个 ThreadLocalMap 实例对象,然后以当前 ThreadLocal 实例作为 key,值为 value 存放到 ThreadLocalMap 中,然后将当前线程对象的 ThreadLocals 赋值为 ThreadLocalMap 对象。

set 方法的重要性在于它确保了每个线程都有自己的变量副本。由于这些变量是存储在与线程关联的映射表中的,所以不同的线程之间的这些变量互不影响。

get方法

get 方法用于获取当前线程中 ThreadLocal 的变量值,同样的还是来看源码:

public T get() {
  //1. 获取当前线程的实例对象
  Thread t = Thread.currentThread();

  //2. 获取当前线程的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
	//3. 获取map中当前ThreadLocal实例为key的值的entry
    ThreadLocalMap.Entry e = map.getEntry(this);

    if (e != null) {
      @SuppressWarnings("unchecked")
	  //4. 当前entitiy不为null的话,就返回相应的值value
      T result = (T)e.value;
      return result;
    }
  }
  //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
  return setInitialValue();
}

 我们来看下 setInitialValue 主要做了些什么事情?

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

该方法的逻辑和 set 方法几乎一样

initialValue 方法

protected T initialValue() {
    return null;
}

这个方法是通过 protected 修饰的,也就意味着 ThreadLocal 的子类可以重写该方法给一个合适的初始值。

这里是 initialValue 方法的典型用法:

private static ThreadLocal<Integer> myThreadLocal = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return 0; // 初始值设置为0
    }
};

整个 setInitialValue 方法的目的是确保每个线程在第一次尝试访问其 ThreadLocal 变量时都有一个合适的值。这种“懒惰”初始化的方法确保了仅在实际需要特定于线程的值时才创建这些值。

remove方法

public void remove() {
	//1. 获取当前线程的ThreadLocalMap
	ThreadLocalMap m = getMap(Thread.currentThread());
 	if (m != null)
		//2. 从map中删除以当前ThreadLocal实例为key的键值对
		m.remove(this);
}

remove 方法的作用是从当前线程的 ThreadLocalMap 中删除与当前 ThreadLocal 实例关联的条目。这个方法用于释放线程局部变量的资源或重置线程局部变量的值。

ThreadLocalMap源码分析 


ThreadLocalMap 是 ThreadLocal 类的静态内部类,它是一个定制的哈希表,专门用于保存每个线程中的线程局部变量。

static class ThreadLocalMap {}

和大多数容器一样,ThreadLocalMap 内部维护了一个 Entry 类型的数组 类型的数组 table,长度为 2 的幂次方。

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
private Entry[] table;

来看下 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 继承了弱引用 WeakReference<ThreadLocal<?>>,它的 value 字段用于存储与特定ThreadLocal 对象关联的值。使用弱引用作为键允许垃圾收集器在不再需要的情况下回收 ThreadLocal 实例。

当我们为 ThreadLocal 变量赋值时,实际上就是以当前 ThreadLocal 实例为 key,值为 Entry 往这个 ThreadLocalMap 中存放。

注意,Entry 的 key 为弱引用,意味着当 ThreadLocal 外部强引用被置为 null (ThreadLocalInstance=null)时,根据可达性分析,ThreadLocal 实例此时没有任何一条链路引用它,所以系统 GC 的时候 ThreadLocal 会被回收。

这样一来,ThreadLocalMap 就会出现 key 为 null 的 Entry,也就没办法访问这些 key 对应的 value,如果线程迟迟不结束的话,这些 key 为 null 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,无法回收就会造成内存泄漏。

当然,如果 thread 运行结束,ThreadLocal、ThreadLocalMap、Entry 没有引用链可达,在垃圾回收时都会被系统回收。但实际开发中,线程为了复用是不会主动结束的,比如说数据库连接池,过大的线程池可能会增加内存泄漏的风险,因此合理配置线程池的大小和线程的存活时间有助于减轻这个问题。

为了避免这个问题,在每次使用完 ThreadLocal 之后,最好明确调用 ThreadLocal 的 remove 方法来删除与当前线程关联的值。这样可以确保线程再次使用时不会存储旧的、不再需要的值。

不过在我们不使用线程池的前提下,即使我们不使用remove方法进行删除,GC也会自动回收value,也就不会造成内存泄漏。

 ThreadLocal的使用场景


ThreadLocal 的使用场景非常多,比如说:

  • 用于保存用户登录信息,这样在同一个线程中的任何地方都可以获取到登录信息。
  • 用于保存数据库连接、Session 对象等,这样在同一个线程中的任何地方都可以获取到数据库连接、Session 对象等。
  • 用于保存事务上下文,这样在同一个线程中的任何地方都可以获取到事务上下文。
  • 用于保存线程中的变量,这样在同一个线程中的任何地方都可以获取到线程中的变量。

下面是一个使用ThreadLocal来保存用户登录信息的示例。这个示例适用于像Web服务器这样的多线程环境,其中每个线程处理一个独立的用户请求。

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getUserTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            log.info("当前员工id:{}", userId);

            //把当前操作人的id存储进局部线程变量
            BaseContext.setCurrentId(userId);

            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }

 上面两个操作实现了把想要的数据在登录时放进线程局部变量

Long userId = BaseContext.getCurrentId();

 如上,用的时候直接调用静态方法进行使用,代码示例如下:

 Orders orders = Orders.builder()
                .userId(BaseContext.getCurrentId())
                .orderTime(LocalDateTime.now())
                .payStatus(Orders.UN_PAID)
                .status(Orders.PENDING_PAYMENT)
                .number(String.valueOf(System.currentTimeMillis()))
                .phone(addressBook.getPhone())
                .consignee(book.getConsignee())
                .build();
 BeanUtils.copyProperties(ordersSubmitDTO , orders);

小结


ThreadLocal 是一个非常有用的工具类,它可以用于保存线程中的变量,这样在同一个线程中的任何地方都可以获取到线程中的变量。但是,ThreadLocal 也是一个非常容易被误用的工具类,如果没有使用好,就可能会造成内存泄漏的问题。

ThreadLocalMap 是 ThreadLocal 的核心,它是一个以 ThreadLocal 实例为 key,任意对象为 value 的哈希表。ThreadLocalMap 使用开放地址法来处理哈希冲突,它的初始容量为 16,加载因子为 2/3,扩容时会将容量扩大为原来的两倍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值