ThreadLocal使用的正确姿势

大家好,我是阿晶,今天来给大家带来在Java并发编程中ThreadLocal的正确使用姿势。

前言

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。这句话从字面上看起来很容易理解,但是真正理解并不是那么容易。

ThreadLocal的官方API解释为:

“该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。”

大概的意思有两点:

1、ThreadLocal提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每个线程的变量不一样,而同一个线程在任何地方拿到的变量都是一致的,这就是所谓的线程隔离。

2、如果要使用ThreadLocal,通常定义为private static类型,最好是定义为private static final类型。

ThreadLocal可以总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

ThreadLocal称线程本地变量,使用其能够将数据封闭在各自的线程中,每一个ThreadLocal能够存放一个线程级别的变量且它本身能够被多个线程共享使用,并且又能达到线程安全的目的,且绝对线程安全,其用法如下所示:

private static final ThreadLocal<String> RESOURCE = new ThreadLocal<String>();

其中RESOURCE代表了一个能够存放String类型的ThreadLocal对象。此时线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

除了线程安全之外,使用ThreadLocal也能够作为一种“方便传参”的工具,在业务逻辑冗长的代码中,同一个参数需要在多个方法之间层层传递,当这种需要传递的参数过多时,代码会显得十分臃肿、丑陋。

原理

首先来看一下ThreadLocal的结构:

ThreadLocal使用的正确姿势

需要关注的有以下这几个方法:

set

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 {

        /**
         * 键值对实体的存储结构
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this 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 的幂
         * The initial capacity -- MUST be a power of two.
         */

        private static final int INITIAL_CAPACITY = 16;

        /**
         * 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */

        private Entry[] table;

        /**
         * ThreadLocalMap 元素数量
         * The number of entries in the table.
         */

        private int size = 0;

        /**
         * 扩容的阈值,默认是数组大小的三分之二
         * The next size value at which to resize.
         */

        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */

        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * Increment i modulo len.
         */

        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */

        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */

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

ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal ,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal,从源码中看到 ThreadLocalMap 其实就是一个简单的Map结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用, value 是 ThreadLocal 存入的值。

ThreadLocalMap 解决 hash 冲突的方式采用的是「线性探测法」,如果发生冲突会继续寻找下一个空的位置。

每个Thread内部都持有一个ThreadLoalMap对象,可以查看Thread源码。

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;

而ThreadLocal中的getMap(t)恰好返回这个threadLocals

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

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

至此,都能够明白ThreadLocal存值的过程了,虽然是按照前言中的用法声明了一个全局常量,但是这个常量在每次设值时实际都是向当前线程的ThreadLocalMap存值,从而确保了数据在不同线程之间的隔离。

get

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


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

protected T initialValue() {
    return null;
}

有了上面的铺垫,这段代码就不难理解了,获取ThreadLocal内的值时,实际上是从当前线程的ThreadLocalMap中以当前ThreadLocal对象作为key取出对应的值,由于值在保存时时线程隔离的,所以现在取值时只会取得当前线程中的值,所以是绝对线程安全的。

remove

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中也是这样处理的,这里就不再展开了。

下面的一张图很好的解释了ThreadLocal的原理:

ThreadLocal使用的正确姿势

ThreadLocal内存泄漏及正确用法

提及ThreadLocal使用的注意事项时,所有的文章都会指出内存泄漏这一风险。

由于ThreadLocalMap中的Entry的key持有的是ThreadLocal对象的弱引用,当这个ThreadLocal对象当且仅当被ThreadLocalMap中的Entry引用时发生了GC,会导致当前ThreadLocal对象被回收;

那么 ThreadLocalMap 中保存的 key 值就变成了 null,而Entry 又被 ThreadLocalMap 对象引用,ThreadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不销毁的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。

下面就来验证一下这个情景,在方法内部声明了一个ThreadLocal对象,为了更好的演示内存泄漏的情景在使用这个对象存值后将方法内取消对其的强引用,并且通过System.gc()触发了一次垃圾回收(准确的说是希望jvm执行一次垃圾回收,不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的),这样再垃圾回收时会将ThreadLocal对象回收,代码如下所示:

package com.itjing.threadlocal;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.lang.reflect.Field;

@SpringBootTest
class SpringbootThreadlocalApplicationTests {

    class User {
        private Long id;
        private String name;

        public User() {
        }

        public User(Long id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + ''' +
                    '}';
        }
    }

    @Test
    public void testMemoryLeak() throws Exception {
        ThreadLocal<User> threadLocal = new ThreadLocal<>();
        threadLocal.set(new User(System.currentTimeMillis(), "lijing"));
        // threadLocal = null;
        // 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时,控制台输出如下:

ThreadLocal使用的正确姿势

ThreadLocal对象并未被回收,将 threadLocal = null;System.gc();放开,控制台输入如下:

ThreadLocal使用的正确姿势

可以看出 key 确实变成了 null 值,而Entry内会一直持有对value的引用,导致value无法被回收,如果当前线程一直在执行未被销毁,则确实会出现内存泄漏(在使用线程池时更容易出现这样的问题)。

分析一下上面的为什么会出现内存泄漏的原因,在上面的代码里,在方法内部声明了一个ThreadLocal对象,该ThreadLocal对象仅有一个方法内部的强引用且生命周期很短,当该方法执行完成之后此ThreadLocal对象在下一次gc时就会被回收,当然可以在方法结束前手动执行一个该对象的remove方法,但是这样就失去了使用ThreadLocal的意义。

由此,出现内存泄漏的原因是失去了对ThreadLocal对象的强引用,避免内存泄漏最简单的方法就是始终保持对ThreadLocal对象的强引用,为每个线程声明一个对ThreadLocal对象的强引用显然是不合适的(太麻烦且缺乏声明的时机),所以,可以将ThreadLocal对象声明为一个全局常量,所有的线程均使用这一常量即可,例如:

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清除。

验证一下:

弱引用key:java.lang.ThreadLocal@6e34f88d,值:thread1
弱引用key:java.lang.ThreadLocal@6e34f88d,值:thread2

可以看出两个线程内对应的Entry的key为同一个对象且即使发生了垃圾回收该对象也不会被回收。

那么是不是说将ThreadLocal对象声明为一个全局常量后使用就没有问题了呢?

当然不是,需要确保在每次使用完ThreadLocal对象后确保要执行一下该对象的remove方法,清除当前线程保存的信息,这样当此线程再被利用时不会取到错误的信息(使用线程池极易出现);

我之前做项目之前就出现过这种场景,从线程池中获取线程,并在每次请求时在当前线程记录下对应的用户信息,结果有一天出现了串号的问题,B用户访问时使用了A用户的信息,这就是在每次请求结束后没有执行remove方法,线程复用时内部还保存着上一个用户的信息,贴上一份使用ThreadLocal的正确姿势:

package com.itjing.threadlocal;

import com.itjing.threadlocal.entity.User;

/**
 * @author lijing
 * @date 2022年05月31日 19:08
 * @description 线程当前用户信息
 */

public class CurrentUser {
    private static final ThreadLocal<User> USER = new ThreadLocal<>();

    private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();

    private static final InheritableThreadLocal<User> INHERITABLE_USER = new InheritableThreadLocal<>();

    private static final InheritableThreadLocal<Long> INHERITABLE_USER_ID = new InheritableThreadLocal<>();

    public static void setUser(User user) {
        USER.set(user);
        INHERITABLE_USER.set(user);
    }

    public static void setUserId(Long id) {
        USER_ID.set(id);
        INHERITABLE_USER_ID.set(id);
    }

    public static User user() {
        return USER.get();
    }

    public static User 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信息清除.

package com.itjing.threadlocal.listener;

import com.itjing.threadlocal.CurrentUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.ServletRequestHandledEvent;

/**
 * @author lijing
 * @date 2022年05月31日 19:12
 * @description 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()
        );
    }
}

可传递给子线程的InheritableThreadLocal

如果在当前线程中开辟新的子线程并希望子线程获取父线程保存的线程本地变量要怎么做呢?

在子线程中声明ThreadLocal对象并将父线程中对应的值存入自然是可以的,但是大可不必如此繁琐,jdk已经提供了一种可传递给子线程的InheritableThreadLocal,实现的原理也很简单,可以在Thread中一窥究竟。

// 持有了一个可传递给子线程的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();
}

这个在上面的 CurrentUser 中也给出了。

使用场景

ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:

  • 线程间数据隔离,各线程的 ThreadLocal 互不影响
  • 方便同一个线程使用某一对象,避免不必要的参数传递
  • 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
  • Spring 事务管理器采用了 ThreadLocal
  • Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
  • ……

总结

主要从源码的角度解析了 ThreadLocal,并分析了发生内存泄漏的原因及正确用法,最后对它的应用场景进行了简单介绍。

ThreadLocal还有其他变种例如FastThreadLocalTransmittableThreadLocalFastThreadLocal主要解决了伪共享的问题比ThreadLocal拥有更好的性能,TransmittableThreadLocal主要解决了线程池中线程复用导致后续提交的任务并不会继承到父线程的线程变量的问题。

关于其变种,我可能会在后面的文章中讲解。


原文始发于微信公众号(程序员阿晶):ThreadLocal使用的正确姿势

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/19601.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!