• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

读懂ThreadLocal的原理和使用场景

武飞扬头像
程序员典籍
帮助1

ThreadLocal 是什么

ThreadLocal 类是用来提供线程内部的局部变量,即线程本地变量。这种变量在多线程环境下访问(通过get和set方法访问)时能够保证各个线程的变量相对独立于其他线程内的变量,不同线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

顾名思义,ThreadLocal 表示线程的“本地变量”,即每个线程都拥有该变量副本,达到人手一份的效果,各用各的,这样就可以避免共享资源的竞争

ThreadLocal 的使用场景

高并发中会存在多个线程同时修改一个共享变量的场景,这就可能会出现线性安全问题。

为了解决线性安全问题,可以通过加锁来实现,例如使用synchronized 或者Lock。但是加锁的方式可能会导致系统变慢。加锁方式如下所示:

学新通

另外一种方式,可以使用ThreadLocal类访问共享变量,这样会在每个线程的本地,都保存一份共享变量的拷贝副本。这是一种“空间换时间”的方案,虽然会让内存占用大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待,从而提高时间效率。

学新通

ThreadLocal 的实现原理

接下来就让我们学习 ThreadLocal 的几个核心方法,来了解ThreadLocal 的实现原理。

方法声明 描述
public void set(T value) 设置当前线程绑定的局部变量
public T get() 获取当前线程绑定的局部变量
Public void remove() 移除当前线程绑定的局部变量

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);
}

方法的逻辑很清晰,具体请看上面的注释。通过源码我们知道 value 是存放在 ThreadLocalMap 里的,当前先把它理解为一个普普通通的 map 即可,也就是说,数据 value 是存放在 ThreadLocalMap 这个容器中的,并且是以当前 ThreadLocal 实例为 key 的

简单看下 ThreadLocalMap 是什么,有个简单的认识就好,后面会具体说的。

首先 ThreadLocalMap 是怎样来的?源码很清楚,是通过getMap(t)进行获取:

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

该方法直接返回当前线程对象 t 的一个成员变量 ThreadLocals:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap ThreadLocals = null;

也就是说ThreadLocalMap 的引用是作为 Thread 的一个成员变量的,被 Thread 进行维护的。回过头再来看 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 方法:

通过当前线程对象 thread 获取该 thread 所维护的 ThreadLocalMap,如果 ThreadLocalMap 不为 null,则以 ThreadLocal 实例为 key,值为 value 的键值对存入 ThreadLocalMap,若 ThreadLocalMap 为 null 的话,就新建 ThreadLocalMap,然后再以 ThreadLocal 为键,值为 value 的键值对存入即可。

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();
}

弄清楚 set 方法的逻辑后,看 get 方法只需要带着逆向思维去看就好,如果是那样存的,反过来去拿就好。代码逻辑请看注释,另外,看下 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 的子类可重写该方法,实现赋值为其他的初始值

总结一下 get 方法:

通过当前线程 thread 实例获取到它所维护的 ThreadLocalMap,然后以当前 ThreadLocal 实例为 key 获取该 map 中的键值对(Entry),如果 Entry 不为 null 则返回 Entry 的 value。如果获取 ThreadLocalMap 为 null 或者 Entry 为 null 的话,就以当前 ThreadLocal 为 Key,value 为 null 存入 map 后,并返回 null。

remove() 方法

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

get、set 方法实现了存数据和读数据的操作,remove 方法实现了如何删数据的操作。删除数据当然是从 map 中删除数据,先获取与当前线程相关联的 ThreadLocalMap,然后从 map 中删除该 ThreadLocal 实例为 key 的键值对即可。

ThreadLocalMap 详解

从上面的分析我们已经知道,数据其实都放在了 ThreadLocalMap 中,ThreadLocal 的 get、set 和 remove 方法实际上都是通过 ThreadLocalMap 的 getEntry、set 和 remove 方法实现的。如果想真正全方位的弄懂 ThreadLocal,势必得再对 ThreadLocalMap 做一番理解。

Entry 数据结构

ThreadLocalMap 是 ThreadLocal 一个静态内部类,和大多数容器一样,内部维护了一个数组(Entry 类型的 table 数组)。

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

通过注释可以看出,table 数组的长度为 2 的幂次方。接下来看下 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,Object 为 value 的键值对,另外需要注意的是这里的ThreadLocal 是弱引用,因为 Entry 继承了 WeakReference,在 Entry 的构造方法中,调用了 super(k)方法,会将 ThreadLocal 实例包装成一个 WeakReferenece。

到这里我们可以用一个图来理解下 Thread、ThreadLocal、ThreadLocalMap、Entry 之间的关系:

学新通

注意上图中的实线表示强引用,虚线表示弱引用。如图所示,每个线程实例中都可以通过 ThreadLocals 获取到 ThreadLocalMap,而 ThreadLocalMap 实际上就是一个以 ThreadLocal 实例为 key,任意对象为 value 的 Entry 数组。

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

需要注意的是,Entry 中的 key 是弱引用,当 ThreadLocal 外部强引用被置为 null(ThreadLocalInstance=null)时,那么系统 GC 的时候,根据可达性分析,这个 ThreadLocal 实例就没有任何一条链路能够引用到它,此时 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。

当然,如果当前 thread 运行结束,ThreadLocal、ThreadLocalMap、Entry 没有引用链可达,在垃圾回收的时候都会被系统回收。在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的。

set 方法

与 ConcurrentHashMap、HashMap 等容器一样,ThreadLocalMap 也是采用散列表进行实现的。

ThreadLocalMap 中使用开放地址法来处理散列冲突,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:

在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。

set 方法的源码如下:

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;
	//根据ThreadLocal的hashCode确定Entry应该存放的位置
    int i = key.ThreadLocalHashCode & (len-1);

	//采用开放地址法,hash冲突的时候使用线性探测
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
		//覆盖旧Entry
        if (k == key) {
            e.value = value;
            return;
        }
		//当key为null时,说明ThreadLocal强引用已经被释放掉,那么就无法
		//再通过这个key获取ThreadLocalMap中对应的entry,这里就存在内存泄漏的可能性
        if (k == null) {
			//用当前插入的值替换掉这个key为null的“脏”entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	//新建entry并插入table中i处
    tab[i] = new Entry(key, value);
    int sz =   size;
	//插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

set 方法的关键部分请看上面的注释

ThreadLocal 的 hashcode

private final int ThreadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode =new AtomicInteger();
  /**
   * Returns the next hash code.
   */
  private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
  }

从源码中我们可以清楚的看到 ThreadLocal 实例的 hashCode 是通过 nextHashCode() 方法实现的,该方法实际上是用一个 AtomicInteger 加上 0x61c88647 来实现的。

0x61c88647 这个数是有特殊意义的,它能够保证 hash 表的每个散列桶能够均匀的分布,这是Fibonacci Hashing

也正是能够均匀分布,所以 ThreadLocal 选择使用开放地址法来解决 hash 冲突的问题。

总结

本文主要讲解了ThreadLocal的作用及基本用法,以及ThreadLocal的实现原理和基础方法。线上环境中,ThreadLocal还有可能引起内存泄漏,这方面内容我们后续接着讲。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhffbcjg
系列文章
更多 icon
同类精品
更多 icon
继续加载