ThreadLocal设计思想浅析

如果你发现你的“此时此地”变得无法忍受并且使你非常不开心,这时你有三种选择:从这种状况中离开,改变它,或者完全接受它。

通过阅读这篇文章,你将可以学习到:

  1. ThreadLocal的使用方法与典型应用场景
  2. ThreadLocal的核心概念——ThreadLocalMap
  3. ThreadLocal的设计思路——映射关系维护、生命周期管理
  4. ThreadLocal使用过程中的那些大坑

接下来进入这些知识的分析和讲解过程。

ThreadLocal的使用方法与典型应用场景

ThreadLocaljava.lang包里面提供的类,正如其名字所示,它可以提供线程独占的变量,每一个线程,都可以拥有一份只有它自己使用的ThreadLocal对象集合。不同线程之间,即使是同名的对象,也不是同一个实例。

该对象,如果不是手动释放,它的生命周期会始终持续到线程运行结束。

使用方法

 

java

代码解读

复制代码

import java.util.concurrent.atomic.AtomicInteger; public class ThreadId { // Atomic integer containing the next thread ID to be assigned private static final AtomicInteger nextId = new AtomicInteger(0); // Thread local variable containing each thread's ID private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() { Override protected Integer initialValue() { return nextId.getAndIncrement(); } }; // Returns the current thread's unique ID, assigning it if necessary public static int get() { return threadId.get(); } }

一个典型的用例如上,使用泛型声明一个静态的ThreadLocal变量,通过覆写initialValue函数,为这个变量赋值,同时,AtomicInteger保证了不同线程之间,生成变量值的唯一性。这样,线程内部通过调用ThreadId.get(),可以获取到该线程唯一的id

可以思考下,如果不借助ThreadLocal,要如何实现上述“为每个Thread赋值唯一的threadId的需求——在Thread类里面增加成员变量threadId,并且在Thread的构造函数里,由一个全局Factory为该threadId赋值(同样是借助AtomicInteger)。不仅实现方案要繁琐许多,而且一旦这个变量不是Integer类型,而是String或者Object呢?势必需要大量全局Factory

典型应用场景

ThreadLocal的应用场景有3个最主要的特征:

  • 并发环境,多个线程需要访问同一个对象
  • 该对象在被调用时,内部状态会发生不可预测的变化
  • 该对象创建成本高

典型的就是SimpleDateFormat,我们知道它一般用于格式化日期字符串,将Date对象转化为String

这边整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

 需要全套面试笔记的【点击此处即可】免费获取

java

代码解读

复制代码

new SimpleDateFormat("yyyyMMdd HHmm").format(date)

SimpleDateFormat对象并非线程安全的,这里截取它代码中的片段,不安全处已经用注释标明:

 

java

代码解读

复制代码

// Called from Format after creating a FieldDelegate private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list calendar.setTime(date); // 注意!这里calendar是成员变量,在并发调用情况下,旧值会被新值覆盖 boolean useDateFormatSymbols = useDateFormatSymbols(); for (int i = 0; i < compiledPattern.length; ) { int tag = compiledPattern[i] >>> 8; int count = compiledPattern[i++] & 0xff; if (count == 255) { count = compiledPattern[i++] << 16; count |= compiledPattern[i++]; } switch (tag) { case TAG_QUOTE_ASCII_CHAR: toAppendTo.append((char)count); break; case TAG_QUOTE_CHARS: toAppendTo.append(compiledPattern, i, count); i += count; break; default: subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols); break; } } return toAppendTo; }

在并发环境下,我们当然可以为每一个线程单独创建一个SimpleDateFormat对象,用于格式化日期。但要知道,SimpleDateFormat对象创建成本是很高的,这显然会对程序运行效率造成负面影响。另一个手段是,在访问该变量的函数上加锁,通过临界区来保证同一时间下只有一条线程使用该对象。方案二同样不利于程序运行效率,rejected!

这时便可以使用ThreadLocal,即降低了重复创建对象造成的内存消耗,又能避免并发调用导致的状态问题。

 

java

代码解读

复制代码

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

ThreadLocal的核心概念

ThreadLocal本质上是通过类似HashMap的键值对结构,来维护Thread独占的实例集合。

核心概念之ThreadLocalMap

ThreadLocalMapThreadLocal的静态内部类,由于是静态,不持有外部对象引用,以下是源码,次要代码已略去。

 

java

代码解读

复制代码

static class ThreadLocalMap { // 继承自WeakReference,将传入的ThreadLocal对象包装为key,value则是对应泛型的值 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 下述capacity、table、size、threshold等,用于实现类似HashMap的数据存储和扩容功能 private static final int INITIAL_CAPACITY = 16; private Entry[] table; private int size = 0; private int threshold; // Default to 0 // 饱汉式,懒加载 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } // 用于子线程拷贝父线程的ThreadLocal对象,createInheritedMap private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (Entry e : parentTable) { 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++; } } } } // 存储KV private void set(ThreadLocal<?> key, Object value) { 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)]) { // for循环找到首个可用entry if (e.refersTo(key)) { // 若已有key,则更新value e.value = value; return; } if (e.refersTo(null)) { // 否则插入 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) // 容量检查 rehash(); } // 使用完一定要remove,否则会导致Entry持有object引用(即使key变为了null,但value仍然指向Object) private void remove(ThreadLocal<?> key) { 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)]) { // Android-changed: Use refersTo(). if (e.refersTo(key)) { e.clear(); expungeStaleEntry(i); return; } } } // 对外暴露的set方法,将ThreadLocal对象加入到当前Thread的ThreadLocalMap中 public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } } // 对外暴露的get方法,可见本质上每个线程自己创建并维护了一个Object 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") T result = (T)e.value; return result; } } return setInitialValue(); } }

ThreadLocalMap与Thread的关系

每一个Thread对象内部,都有2个Map,分别是自身的threadLocals以及继承自父亲的inheritableThreadLocals

 

java

代码解读

复制代码

// Thread.java /* 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;

Entry是WeakReference的子类

Entry是一个维护了键值对的数据包装类,Key为ThreadLocal<Object>对象,Value为该变量对应的Object对象实例。为了避免对ThreadLocal对象造成强引用,使Entry继承自WeakReference<ThreadLocal>

类HashMap实现ThreadLocal对象集合

ThreadLocalMap内部实际上维护了一个Entry[]数组,命名为table,其初始大小16,超过2/3则触发扩容。在插入时,index的计算方式为key.threadLocalHashCode & (len - 1),其中key就是ThreadLocal类型的对象。

ThreadLocal的设计思路——映射关系维护、生命周期管理

映射关系维护

根据上述分析,我们可以将ThreadThreadLocal的关系用下图表示:

image.png

这里可以明确一个核心思想,即ThreadLocal机制并不是把一个对象分散给多个Thread使用,实际上它是对每个Thread都创建一个该对象实例,并且交给Thread自身进行维护。理解了这句话,也就理解了ThreadLocal机制的本质。

ThreadLocal对象设值有两种方式,一是通过覆写initialValue()函数,被动初始化;二是主动调用set()函数进行初始化。

生命周期管理

已知如上的引用关系中,存在2条引用链(这里Object对象代指ThreadLocal通过泛型包装的对象):

image.png

  • Thread对象到ThreadLocal对象的虚引用:Thread->ThreadLocalMap->Entry->WeakRef<ThreadLocal>
  • Thread对象到Object对象的强引用:Thread->ThreadLocalMap->Entry->Object

建立这两种引用关系的时机很好理解,就是向ThreadLocalMap里面添加对象时,也就是覆写initialValue()或者调用set()的时机。当ThreadLocal对象使用完毕后,该Entry并不会自动移除,而是仍然保留在ThreadThreadLocalMap成员变量里(这不难理解,Thread本身又不知道这个对象后续是否要使用)。

由于Entry对于ThreadLocal对象的引用为WeakRef,当JVM发生GC时会释放该引用,从而使EntryKey变为null。在源代码里,将这种Key为nullEntry称为StaleEntry(过期条目)。很明显,过期条目持有了Object的强引用,如果不进行释放,会导致严重的内存泄漏。ThreadLocal类里面提供了expungeStaleEntry(i)函数用来清理StaleEntry

 

java

代码解读

复制代码

// ThreadLocal.java 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; }

调用上述函数,就会遍历清理全部的StaleEntry,以下场景会触发这个流程:

  • 场景一Thread运行结束,释放ThreadLocalMap,其中的Entry自然就被释放了
  • 场景二:在调用set(Object)时,恰好命中了一个StaleEntry
  • 场景三:主动对ThreadLocal对象调用remove()函数

场景一不适用于线程池的场景,此时线程长期存活,无法释放。

场景二随机性太大,需要计算Key的hash值时恰好命中上一个被释放的Entry,不可控。

因此,最稳妥的方式是场景三。这也是在使用ThreadLocal时容易出错的地方,没有主动调用release()方法,导致有内存泄漏的风险。

ThreadLocal使用过程中的那些大坑

到这里其实也没啥好讲的,只要记住“用完要释放”,就能避免99%的问题。

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocalJava 中的一个类,它提供了一种线程局部变量的机制。线程局部变量是指每个线程都有自己的变量副本,每个线程对该变量的访问都是独立的,互不影响。 ThreadLocal 主要用于解决多线程并发访问共享变量时的线程安全问题。在多线程环境下,如果多个线程共同访问同一个变量,可能会出现竞争条件,导致数据不一致或者出现线程安全问题。通过使用 ThreadLocal,可以为每个线程提供独立的副本,从而避免了线程安全问题。 ThreadLocal 的工作原理是,每个 Thread 对象内部都维护了一个 ThreadLocalMap 对象,ThreadLocalMap 是一个 key-value 结构,其中 key 是 ThreadLocal 对象,value 是该线程对应的变量副本。当访问 ThreadLocal 的 get() 方法时,会根据当前线程获取到对应的 ThreadLocalMap 对象,并从中查找到与 ThreadLocal 对象对应的值。如果当前线程尚未设置该 ThreadLocal 对象的值,则会通过 initialValue() 方法初始化一个值,并将其存入 ThreadLocalMap 中。当访问 ThreadLocal 的 set() 方法时,会将指定的值存入当前线程对应的 ThreadLocalMap 中。 需要注意的是,ThreadLocal 并不能解决共享资源的并发访问问题,它只是提供了一种线程内部的隔离机制。在使用 ThreadLocal 时,需要注意合理地使用,避免出现内存泄漏或者数据不一致的情况。另外,由于 ThreadLocal 使用了线程的 ThreadLocalMap,因此在使用完 ThreadLocal 后,需要手动调用 remove() 方法清理对应的变量副本,以防止内存泄漏。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值