public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。
说了不少废话,现在就步入正题了,让我们揭开ThreadLocal的庐山真面目。
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }如果能够搞懂这块代码,就能够明白ThreadLocal到底是怎么实现的了。这块代码其实很有意思,我们发现在向ThreadLocal中存放值时需要先从当前线程中获取ThreadLocalMap,最后实际是要把当前ThreadLocal对象作为key、要存入的值作为value存放到ThreadLocalMap中,那我们就不得不先看一下ThreadLocalMap的结构。
static class ThreadLocalMap { /** * 堆代码 duidaima.com * 键值对实体的存储结构 */ static class Entry extends WeakReference<ThreadLocal<?>> { // 当前线程关联的 value,这个 value 并没有用弱引用追踪 Object value; /** * 构造键值对 * * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用 * @param v v 作 value */ Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 初始容量,必须为 2 的幂 private static final int INITIAL_CAPACITY = 16; // 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂 private Entry[] table; // ThreadLocalMap 元素数量 private int size = 0; // 扩容的阈值,默认是数组大小的三分之二 private int threshold; }ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal,从源码中看到 ThreadLocalMap 其实就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal内存入 的值。ThreadLocalMap 解决 hash 冲突的方式采用的是「线性探测法」,如果发生冲突会继续寻找下一个空的位置。
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;至此,我们都能够明白ThreadLocal存值的过程了,虽然我们是按照前言中的用法声明了一个全局常量,但是这个常量在每次设置时实际都是向当前线程的ThreadLocalMap内存值,从而确保了数据在不同线程之间的隔离。
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(); }有了上面的铺垫,这段代码就不难理解了,获取ThreadLocal内的值时,实际上是从当前线程的ThreadLocalMap中以当前ThreadLocal对象作为key取出对应的值,由于值在保存时线程隔离的,所以现在取值时只会取得当前线程中的值,所以是绝对线程安全的。
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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }remove将ThreadLocal对象关联的键值对从Entry中移除,正确执行remove方法能够避免使用ThreadLocal出现内存泄漏的潜在风险,int i = key.threadLocalHashCode & (len-1)这行代码很有意思,从一个集合中找到一个元素存放位置的最简单方法就是利用该元素的hashcode对这个集合的长度取余,如果我们能够将集合的长度限制成2的整数次幂就能够将取余运算转换成hashcode与[集合长度-1]的与运算,这样就能够提高查找效率,HashMap中也是这样处理的,这里就不再展开了。
@Test public void loop() throws Exception { for (int i = 0; i < 1; i++) { ThreadLocal<SysUser> threadLocal = new ThreadLocal<>(); threadLocal.set(new SysUser(System.currentTimeMillis(), "李四")); // threadLocal = null; //System.gc(); printEntryInfo(); } //System.gc(); //printEntryInfo(); } private void printEntryInfo() throws Exception { Thread currentThread = Thread.currentThread(); Class<? extends Thread> clz = currentThread.getClass(); Field field = clz.getDeclaredField("threadLocals"); field.setAccessible(true); Object threadLocalMap = field.get(currentThread); Class<?> tlmClass = threadLocalMap.getClass(); Field tableField = tlmClass.getDeclaredField("table"); tableField.setAccessible(true); Object[] arr = (Object[]) tableField.get(threadLocalMap); for (Object o : arr) { if (o != null) { Class<?> entryClass = o.getClass(); Field valueField = entryClass.getDeclaredField("value"); Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent"); valueField.setAccessible(true); referenceField.setAccessible(true); System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o))); } } }在不发生GC时,控制台输出如下:
private static final ThreadLocal<String> RESOURCE = new ThreadLocal<>(); @Test public void multiThread() { Thread thread1 = new Thread(() -> { RESOURCE.set("thread1"); System.gc(); try { printEntryInfo(); } catch (Exception e) { e.printStackTrace(); } }); Thread thread2 = new Thread(() -> { RESOURCE.set("thread2"); System.gc(); try { printEntryInfo(); } catch (Exception e) { e.printStackTrace(); } }); thread1.start(); thread2.start(); }按照上面的方式声明ThreadLocal对象后,所有的线程共用此对象,在使用此对象存值时会把此对象作为key然后把对应的值作为value存入到当前线程的ThreadLocalMap中,由于此对象始终存在着一个全局的强引用,所以其不会被垃圾回收,调用remove方法后就能够将此对象关联的Entry清除。
package com.cube.share.thread.config; import com.cube.share.thread.entity.SysUser; /** * @堆代码 duidaima.com * @date 2023/9/21 14:50 * <p> * 线程当前用户信息 */ public class CurrentUser { private static final ThreadLocal<SysUser> USER = new ThreadLocal<>(); private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>(); private static final InheritableThreadLocal<SysUser> INHERITABLE_USER = new InheritableThreadLocal<>(); private static final InheritableThreadLocal<Long> INHERITABLE_USER_ID = new InheritableThreadLocal<>(); public static void setUser(SysUser sysUser) { USER.set(sysUser); INHERITABLE_USER.set(sysUser); } public static void setUserId(Long id) { USER_ID.set(id); INHERITABLE_USER_ID.set(id); } public static SysUser user() { return USER.get(); } public static SysUser inheritableUser() { return INHERITABLE_USER.get(); } public static Long inheritableUserId() { return INHERITABLE_USER_ID.get(); } public static Long userId() { return USER_ID.get(); } public static void removeAll() { USER.remove(); USER_ID.remove(); INHERITABLE_USER.remove(); INHERITABLE_USER_ID.remove(); } }我们可以通过切面或者请求监听器在请求结束时将当前线程保存的ThreadLocal信息清除。
/** * @堆代码 duidaima.com * @date 2023/9/21 15:12 * <p> * ServletRequest请求监听器 */ @Component @Slf4j public class ServletRequestHandledEventListener implements ApplicationListener<ServletRequestHandledEvent> { @Override public void onApplicationEvent(ServletRequestHandledEvent event) { CurrentUser.removeAll(); log.debug("清除当前线程用户信息,uri = {},method = {},servletName = {},clientAddress = {}", event.getRequestUrl(), event.getMethod(), event.getServletName(), event.getClientAddress()); } }
//持有了一个可传递给子线程的ThreadLocalMap ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; //线程创建时都会执行这个初始化方法,inheritThreadLocals表示是否需要在构造时从父线程中继承thread-locals,默认为true private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { //忽略了一部分代码 setPriority(priority); //从父线程中继承thread-locals if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID(); }
5.Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal