在java中,ThreadLocal算是一个比较神秘的类了,我相信如果没接触过Spring源码或者一直在做CRUD操作的开发者一般也很少能接触到它,但是面试时却经常被问到,其实对于更深层次的学习,是需要去了解其使用和实现的,今天就一起了解下这个神秘的ThreadLocal
JDK版本号:1.8.0_171
如果你还有印象的话,应该记得我之前对Thread源码分析时曾经提及过ThreadLocal(可以参考我之前的文章),在Thread中有两个成员变量:
/* 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;
既然放在了Thread里,那么就说明这两个变量在整个线程的生命周期中都可以被使用,那么jdk中是如何使用这两个变量的呢?
在进行讲解说明之前提出一个问题供大家思考:
你可能会想,这不很简单吗, 直接创建个局部变量不就ok了 ,是的,我们可能直接从controller层开始,根据需求生成对应的变量A,每个线程变量A对应的值都不一样,ok,生成完毕,每次调用下层方法时传参进去,这样每个方法都可以用到了,如果封装的好点,设置个map或创建一个规范的对象保存,恩,完美。这确实是一种解决方式,也就是创建局部变量,通过方法参数进行传递,而且这也是我们进行开发时所经常使用的方式
然而你可以想象到这种方式所带来的问题,我们夸张点,如果在一次请求中有成千上百个方法都用到了,那么每个方法参数中都需要我们进行传递,比较麻烦,维护也比较难受,那么为什么不用全局变量呢?因为我们的前提条件就是不同线程操作的是不同的变量,尽管变量名称A一样,但是其对应的值在不同线程中是不同的
好,那么有些人就想了, 设置个map,以线程id为key,变量A为值 ,ok,这样每个线程获取变量A时就获取到自己线程id对应的变量A了,确实可以解决,那么这里的map应该是设置成全局变量了,既然是全局变量,那么又会带来多线程并发问题,需要保证数据的一致性,那么用个并发安全的map就ok了吧
然而此时停下来我们仔细思考下,我们到底做了什么?本来我们用局部变量就能解决的,但是现在我们必须要设置全局变量,同时还要考虑并发安全问题,对于使用的变量A而言,它在线程间原本就应该是隔离的,每个请求生成自己对应的值,然后操作,不需要与其他线程有关联,然而设置并发map关联彼此使得我们需要考虑更多的问题,是不是有点得不偿失
jdk本身也没有采用这种方式来完成这种场景下的操作,那么有没有一种方式可以创建一个变量,可以使其在线程间隔离同时在方法或类之间共享呢?答案是有的,也就是通过jdk提供的ThreadLocal创建线程级变量
ThreadLocal提供了线程级变量,每个线程都可以通过set(),get()和remove()来对这个变量进行操作,但不会和其他线程的变量有冲突,实现了线程的数据隔离,避免数据共享。也就是说ThreadLocal中保存的变量属于当前线程,该变量对其他线程而言是隔离的,这里的隔离就和我上面思考中所说的变量A在不同的线程中生成的值是不同的情况一样
首先请大家理解清楚:
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。同时使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。 最重要的是每个线程有自己的线程级变量,其他线程是无法操作的,线程中的ThreadLocal变量也没有用map或其他结构将所有线程的变量关联起来,所以他们就和局部变量一样彼此之间是毫无关系的
可能有些读者会被绕晕,理解ThreadLocal的关键在于你要明白, 它就是一个变量 ,也就是说就像我们设置局部变量在方法内部使用一样,我们通过ThreadLocal设置了线程级变量,在整个线程生命周期中被使用,就当变量使用,只是和局部变量作用域不同,同时通过ThreadLocal操作而已,先不要去考虑其内部实现,也不要在一个线程中想通过ThreadLocal来获取其他线程的变量值,因为根本就获取不到
ThreadLocal的使用并不是很复杂,在注释的地方我们可以看到作者举了个例子,可以参考我下列的代码理解,每个线程生成了各自对应的id,只要是在同一个线程内通过get()方法获取的必定是同一个数值
private static ExecutorService service = Executors.newFixedThreadPool(10); private static final AtomicInteger nextId = new AtomicInteger(0); private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(nextId::getAndIncrement); public static void main(String[] args) { for (int i = 0; i < 10; i++) { service.submit(() -> { System.out.println(Thread.currentThread().getName() + " " + threadId.get()); threadId.remove(); }); } service.shutdown(); }
那么如果我需要使用多个线程级变量呢?该如何实现呢?那就创建多个ThreadLocal即可,可参考下面的代码:
private static ExecutorService service = Executors.newFixedThreadPool(10); private static final AtomicInteger nextId = new AtomicInteger(0); private static final ThreadLocal<Integer> threadIdOne = ThreadLocal.withInitial(nextId::getAndIncrement); private static final ThreadLocal<Integer> threadIdTwo = ThreadLocal.withInitial(nextId::getAndIncrement); private static final ThreadLocal<Integer> threadIdThree = ThreadLocal.withInitial(nextId::getAndIncrement); public static void main(String[] args) { for (int i = 0; i < 10; i++) { service.submit(() -> { System.out.println(Thread.currentThread().getName() + " " + threadIdOne.get() + " " + threadIdTwo.get() + " " + threadIdThree.get()); threadIdOne.remove(); threadIdTwo.remove(); threadIdThree.remove(); }); } service.shutdown(); }
从上面示例代码中可以很清楚的看到,当我们使用ThreadLocal时就按照使用变量的操作方式即可,使用几个就创建几个,同时需要注意的问题是 在线程使用完毕之后最好进行remove操作,防止错误使用和内存泄漏的风险 。在实际的服务中我们经常使用的是线程池来进行操作,如果线程A被请求1使用设置了变量p,然后线程A还回线程池接着被请求2使用,那么这里请求1设置的变量p还是存在着的,所以最好使用完毕之后进行remove操作,可以将上述示例的线程提交任务个数修改为20,同时注释掉remove操作,大家可以看看结果。至于内存泄漏风险在讲解ThreadLocalMap时将进行说明
在源码学习之前,我们需要理清楚Thread,ThreadLocal,ThreadLocalMap三者之间的关联,以更好的理解其源码实现,简单说明下,ThreadLocalMap内部使用了Entry数组,可以简单理解成map,key保存ThreadLocal,value保存我们实际设置的变量值,对于三者的关系可能有点绕,大家可以参考下图理解
下图中展示的是创建了2个ThreadLocal变量,在线程A和线程B中三者的关联关系,请各位务必仔细思考理解,当你理解了这个关系图,其实也就基本掌握了ThreadLocal的内部实现
一个Thread对应一个ThreadLocalMap,一个ThreadLocalMap中对应多个ThreadLocal,一个ThreadLocal对应一个Entry,那么这就说明,一个Thread可以设置多个ThreadLocal
第一点,参考上面的代码示例和关系图,我们不添加static修饰符会怎么样呢?比如上面的ThreadLocal-1,每个线程中创建一个ThreadLocal-1实例,然后使用,ThreadLocal-2同理,使用上是没问题的,但是实际上我们不需要创建多个,所有线程共用一个ThreadLocal-1实例就可以,因为没有任何线程对ThreadLocal实例本身进行操作,我们需要的仅仅是把它当作key,使用static就能保证整个应用程序中只有一个ThreadLocal实例,而我们也确实只需要一个实例,提高效率,节省内存
第二点,由于ThreadLocalMap中的Entry使用了ThreadLocal的弱引用作为key,如果线程还未结束,但是对应的ThreadLocal已经被回收了,那么会造成内存泄漏,定义为static则不会使得ThreadLocal被回收掉
/** * 每个ThreadLocal实例都有一个对应的threadLocalHashCode * 这个值将会用于在ThreadLocalMap中找到ThreadLocal对应的value值 */ private final int threadLocalHashCode = nextHashCode(); /** * 原子int类,下一个hashcode,原子更新 */ private static AtomicInteger nextHashCode = new AtomicInteger(); /** * 魔数0x61c88647,hashcode生成增量 */ private static final int HASH_INCREMENT = 0x61c88647;
nextHashCode和HASH_INCREMENT使用static修饰,属于类变量,只有threadLocalHashCode是和ThreadLocal实例绑定的,通过nextHashCode()方法在实例创建时就生成了对应的threadLocalHashCode,便于散列查找。其中的魔数0x61c88647是比较特殊的,是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突,不深入研究,可自行查找资料
SuppliedThreadLocal是JDK8新增的内部类,只是扩展了ThreadLocal的初始化值的方法而已,允许使用JDK8新增的Lambda表达式赋值。需要注意的是,函数式接口Supplier不允许为null,使用方法可参考上面的使用示例
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> { private final Supplier<? extends T> supplier; SuppliedThreadLocal(Supplier<? extends T> supplier) { this.supplier = Objects.requireNonNull(supplier); } @Override protected T initialValue() { return supplier.get(); } }
public ThreadLocal() { }
可以看到其构造方法没有进行任何操作,本质上ThreadLocal也只是为了ThreadLocalMap而服务的,有点类似代理工具类,这样说来在实例化时仅仅生成了对应的threadLocalHashCode变量
我们需要注意的是以下这些方法都是在操作ThreadLocal,通过ThreadLocal进行Thread中的threadLocals(ThreadLocalMap)的增删改查,也就是操作前言中描述的Thread中的变量
创建ThreadLocal实例时生成其对应的hashcode,每次原子增加HASH_INCREMENT的大小
private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
返回当前线程的ThreadLocal初始设置值。这个方法在当前线程第一次调用ThreadLocal.get方法时进行调用,如果之前已经通过set方法设置过值,则不会调用。这个方法需要我们自行实现,来完成定制操作,也就是我们希望ThreadLocal在每个线程中初始化值不同时可以进行定制,也就是上面示例中那样的操作
protected T initialValue() { return null; }
Lambda表达式赋值,可参考上面示例
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); }
获取当前线程的ThreadLocalMap中对应当前ThreadLocal的Entry,如果ThreadLocalMap还未初始化或当前ThreadLocal的Entry为空则调用setInitialValue,从此也能看出其使用的是懒加载,用到时才进行初始化操作
public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程对应的ThreadLocalMap ThreadLocalMap map = getMap(t); // 非空 if (map != null) { // 获取ThreadLocalMap中对应当前ThreadLocal的Entry ThreadLocalMap.Entry e = map.getEntry(this); // 非空则获取对应的value if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // map还未初始化或当前ThreadLocal的Entry为空则调用 return setInitialValue(); }
初始化操作,返回初始化的值
private T setInitialValue() { // 调用自定义初始化方法 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) // 已经初始化,则set操作 map.set(this, value); else // 未初始化则初始化并赋值 createMap(t, value); return value; }
set操作与setInitialValue类似,只是value是外部传入的
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
移除当前线程ThreadLocalMap对应的ThreadLocal的Entry,如果当前线程调用了remove之后又调用get,则会重新调用initialValue,可参考上面的get方法
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) // 移除Entry m.remove(this); }
获取当前Thread对应的ThreadLocalMap
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
对线程Thread的threadLocals变量进行初始化操作同时赋值firstValue
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
继承父线程的ThreadLocalMap操作方式,可参考Thread.inheritableThreadLocals
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
子类InheritableThreadLocal实现其方法,可参考其源码,在ThreadLocalMap私有的构造方法中遍历父线程中的ThreadLocalMap时使用,继承父线程中的ThreadLocalMap
T childValue(T parentValue) { throw new UnsupportedOperationException(); }
ThreadLocal本质上就是用来实现一个线程级变量,内部实现还是非常巧妙的,在使用上需要注意,使用static修饰,最后使用完一定要remove,同时需要考虑使用场景,没必要使用时不要乱用。其也是一种避免共享的方案,没有共享,就没有并发问题
需要理解的知识还是不少的,如果还比较困惑,仔细思考下Thread,ThreadLocal,ThreadLocalMap三者的关系图,对于ThreadLocalMap的分析将在下篇文章中进行讲解
以上内容如有问题欢迎指出,笔者验证后将及时修正,谢谢