前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ThreadLocal的使用及原理

ThreadLocal的使用及原理

作者头像
Erwin
发布2020-08-02 22:48:09
8440
发布2020-08-02 22:48:09
举报
文章被收录于专栏:啸天"s blog啸天"s blog

概要

如果你还不知道threadlocal,那你就要了解一下,相信你一定会用到它。

作用

threadlocal最大作用就是提供线程级别的变量生命周期。 试想,如果你需要一个变量在一个线程的生命周期内都可以访问到,在不使用threadlocal的前提下你会怎么做?你或许这样做

  1. 提供一个类级别或者静态变量 但是这个方法大家很容易就想到在高并发时会出问题。
  2. 把这个局部变量一直传递下去 但是如果你要调用的方法层次很深呢?难道你对每个方法都增加一个参数吗?显然不实际。

所以threadlocal就是提供了一个可行的方案,使得这个变量可以随时访问到,并且不会跟其他线程产生冲突。

使用

threadlocal的使用很简单,就是一个get, set。

public class ThreadLocalTest {

 //定义一个ThreadLocal的变量, 需要指定类型
 public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

 @Before
 public void init(){
 #set值进去
 threadLocal.set("test");
 }

 @Test
 public void test() {
 //在需要时get出来
 System.out.println("threadLocal's value=" + threadLocal.get());
 }

}

实现原理

set

我们先从set方法入手看看做了手脚。

public void set(T value) {
       //取出当前线程
       Thread t = Thread.currentThread();
       //根据当前线程获取ThreadLocalMap。从getMap方法可以看到这个ThreadLocalMap就是保存在Thread对象里面
       ThreadLocalMap map = getMap(t);
       if (map != null)
 #map已经存在就是set进去
 map.set(this, value);
       else
 #不存在新建一个map
 createMap(t, value);
 }

   //getMap方法
   ThreadLocalMap getMap(Thread t) {
 return t.threadLocals;
 }

   //createMap方法
   //注意,这里的this指的是ThreadLocal对象
   void createMap(Thread t, T firstValue) {
 t.threadLocals = new ThreadLocalMap(this, firstValue);
 }

我们再看看ThreadLocalMap的创建及其他方法。ThreadLocalMap是定义在ThreadLocal里的一个静态类。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
 table = new Entry[INITIAL_CAPACITY];
 //通过ThreadLocal的hashCode确定index,这个我们稍后再说
 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
 //重点,把ThreadLocal对象作为key存到了ThreadLocalMap的Entry里
 table[i] = new Entry(firstKey, firstValue);
 size = 1;
 setThreshold(INITIAL_CAPACITY);
 }

set方法比较简单,跟普通的Map差不多,把key和value set进去。 里面还包含了清理key为null的Entry对象的一些操作。

private void set(ThreadLocal<?> key, Object value) {

 // We don't use a fast path as with get() because it is at
 // least as common to use set() to create new entries as
 // it is to replace existing ones, in which case, a fast
 // path would fail more often than not.

 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)]) {
 ThreadLocal<?> k = e.get();

 if (k == key) {
 e.value = value;
 return;
 }

 if (k == null) {
 replaceStaleEntry(key, value, i);
 return;
 }
 }

 tab[i] = new Entry(key, value);
 int sz = ++size;
 if (!cleanSomeSlots(i, sz) && sz >= threshold)
 rehash();
 }

get

public T get() {
 //取当前线程
 Thread t = Thread.currentThread();
 //取出线程的ThreadLocalMap,跟上面是一样的
 ThreadLocalMap map = getMap(t);
 if (map != null) {
 //根据当前对象ThreadLocal取出ThreadLocalMap.Entry
 ThreadLocalMap.Entry e = map.getEntry(this);
 if (e != null) {
 @SuppressWarnings("unchecked")
 T result = (T)e.value;
 return result;
 }
 }
 //如果map为null则做初始化,跟上面的createMap差不多
 return setInitialValue();
 }

图解关系

upload successful
upload successful

可以看出相同的ThreadLocal在不同的线程有不同的值。

主要记住ThreadLocal是作为ThreadLocalMap的key,可能开始有点绕,但是慢慢思考,理清它们的关系就行了。

两个问题

内存泄漏 ?

这是一个对ThreadLocal来说老生常谈的问题了。那使用ThreadLocal为什么会导致内存泄漏?还有我们应该怎么去避免?是我们应该关注的两个点。

原因

首先,我这里假设大家对java的内存回收机制和引用(Reference)有一定的了解。如果不知道,请自行google了。

我们先看看ThreadLocalMap的Entry的定义

//对key使用了WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
 /** The value associated with this ThreadLocal. */
 Object value;
 Entry(ThreadLocal<?> k, Object v) {
 super(k);
 value = v;
 }
 }

为什么要使用WeakReference? 网上很多的说法都说是使用了弱引用就会被GC一句带过,我觉得很多都说得不清不楚的。我通过自己的理解向大家解析一下: 其实很简单,从变量的作用域及引用关系的角度出发思考。试想如果一个ThreadLocal定义为一个类实例的变量或者是一个方法内的局部变量,那么当这个类实例被销毁了或方法退出了,在理想的情况下,垃圾回收器应该回收掉这个ThreadLocal是吧,毕竟它的生命周期已经完结了,但是如果这时ThreadLocalMap还是持有这个ThreadLocal的强引用的话,这个ThreadLocal就不会被回收,直到这个ThreadLocalMap被销毁或者这个线程被销毁。 说白了,从上面那个图看出,这样的设计导致的结果是这个ThreadLocal的生命周期跟线程的生命周期挂上钩了。

同时,这里又会出现另外一种内存泄漏的问题,即使ThreadLocal回收了,但是value没有被回收,还是会导致内存泄漏。但是你没办法把value设置为WeakReference,因为value不是你的,不归你管。

如何防止

ThreadLocal采用如下解决内存泄漏,看expungeStaleEntry方法。

/**
 * Expunge a stale entry by rehashing any possibly colliding entries
 * lying between staleSlot and the next null slot.  This also expunges
 * any other stale entries encountered before the trailing null.  See
 * Knuth, Section 6.4
 *
 * @param staleSlot index of slot known to have null key
 * @return the index of the next null slot after staleSlot
 * (all between staleSlot and this slot will have been checked
 * for expunging).
 */
 private int expungeStaleEntry(int staleSlot) {
 Entry[] tab = table;
 int len = tab.length;

 // expunge entry at staleSlot
 tab[staleSlot].value = null;
 tab[staleSlot] = null;
 size--;

 // Rehash until we encounter null
 Entry e;
 int i;
 for (i = nextIndex(staleSlot, len);
 (e = tab[i]) != null;
 i = nextIndex(i, len)) {
 ThreadLocal<?> k = e.get();
 if (k == null) {
 e.value = null;
 tab[i] = null;
 size--;
 } else {
 int h = k.threadLocalHashCode & (len - 1);
 if (h != i) {
 tab[i] = null;

 // Unlike Knuth 6.4 Algorithm R, we must scan until
 // null because multiple entries could have been stale.
 while (tab[h] != null)
 h = nextIndex(h, len);
 tab[h] = e;
 }
 }
 }
 return i;
 }

在ThreadLocal的源码你会看到很多清洗数据的代码最终都会调用到这个方法。这个方法主要逻辑,简单来说就是当key为null,把value也设置为null,从而让value也被回收。 这个方法的触发点有很多,当对ThreadLocal进行set,get,remove等操作时都会。

容器(如tomcat,netty)一般都是使用线程池处理用户到请求,此时用ThreadLocal要特别注意内存泄漏的问题,一个请求结束了,处理它的线程也结束,但此时这个线程并没有死掉,它只是归还到了线程池中,这时候应该清理掉属于它的ThreadLocal信息。

所以我们使用ThreadLocal一个比较好的习惯是在finally块调用remove方法。

hashcode和0x61c88647?

既然ThreadLocal用map就避免不了冲突的产生。

在ThreadLocalMap的构造方法中,我们可以看到以下代码

//table的下标的计算方式
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);

//threadLocalHashCode的定义
private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
 return nextHashCode.getAndAdd(HASH_INCREMENT);
 }

 /**
 * 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.
 */
   //HASH_INCREMENT的十进制为1640531527
   private static final int HASH_INCREMENT = 0x61c88647;

从代码可以看出每个ThreadLocal的实例的threadLocalHashCode的差值为0x61c88647这么多,那为什么要这样做呢?

这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。 斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647。 通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。 ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。。为了优化效率。

简单来说就是在table[]的size为2的次幂情况下,取模会得到均匀分布。

一个优化点

从上面得知,ThreadLocal的Map可能会产生冲突,解决冲突的办法是线性探测。 而Netty的FastThreadLocal的利用了一个自增序号来作为下标,避免了冲突的产生。

public FastThreadLocal() {
 index = InternalThreadLocalMap.nextVariableIndex();
 }

 public static int nextVariableIndex() {
 int index = nextIndex.getAndIncrement();
 if (index < 0) {
 nextIndex.decrementAndGet();
 throw new IllegalStateException("too many thread-local indexed variables");
 }
 return index;
 }

参考资料

https://juejin.im/post/5b5ecf9de51d45190a434308

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-08-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概要
  • 作用
  • 使用
  • 实现原理
    • set
      • get
        • 图解关系
        • 两个问题
          • 内存泄漏 ?
            • 原因
            • 如何防止
          • hashcode和0x61c88647?
          • 一个优化点
          • 参考资料
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档