欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

java多线程:ThreadLocal详解

时间:2023-07-05

场景: 登录用户的信息保存与获取问题。
在常规的系统设计中,后端系统通常会有一个很长的调用链路(Controller->Service->Dao)。
通常用户在登陆之后,用户信息会保存在session或token中。但假如我们在controller、service及service的多个调用方法中都要用到用户信息相关,我们可以将User对象作为参数进行方法传递,也就是将User作为context上下文。但这样极其繁琐,对于调用链路长的情况也不够优雅简洁;同时若调用链涉及到第三方库,重写的方法无法修改参数的情况下,对象就传递不进去了。我们也不能直接将User对象保存为static,因为在多个用户访问的情况的会有并发的问题。这时候我们就可以用上ThreadLocal对象,进行全局存储用户信息。

提出问题:

ThreadLocal是什么?用来解决什么问题?ThreadLocal的使用ThreadLocal的底层实现ThreadLocal的内存泄漏问题


ThreadLocal是什么

ThreadLocal是一个保存线程局部变量的工具,每一个线程都可以独立地通过ThreadLocal保存与获取自己的变量,而该变量不会受其它线程的影响。

ThreadLocal的使用

ThreadLocal主要对外提供三个方法:get(), set(T)和remove()。
通常我们将threadLocal对象设置为static,以便在全局都可获取。
set(T):线程填充只属于自己线程的数据,其他线程无法获取。
get():线程获取自己set的数据。
remove():线程移除自己设置的值。

public class Test { public static ThreadLocal threadLocal = new ThreadLocal<>(); public static void main(String[] args){ Thread thread1 = new Thread(()->{ User user = new User(10, "jun"); // 模拟出用户1 threadLocal.set(user); playGame(); // 用户1玩游戏 }, "线程1"); Thread thread2 = new Thread(()->{ User user = new User(20, "ge"); // 模拟出用户2 threadLocal.set(user); playGame(); // 用户2玩游戏 }, "线程2"); thread1.start(); thread2.start(); } public static void playGame(){ int age = threadLocal.get().getAge(); // 模拟业务逻辑,登录用户年龄判断的业务逻辑 if(age < 18){ System.out.println("Sorry, 您未满18岁,当前" + age + "岁,不能参与当前游戏!当前线程为:" + Thread.currentThread().getName()); return; } System.out.println("您已满18岁,当前" + age + "岁,玩得愉快!当前线程为:" + Thread.currentThread().getName()); }}

我们模拟创建两个用户登录后,保存进threadLocal中,再分别执行playGame方法。在上述方法中,我们直接根据threadLocal就正确地获取了线程所属的user对象,而没有在方法上传递参数。
如上,我们便解决了调用链路过长时参数传递的繁琐,免去了方法参数传递的过程。每个线程调用threadLocal的get方法时,获取的都是自己set进去的值,解决了并发的问题。

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); }ThreadLocalMap getMap(Thread t) { return t.threadLocals; }

看到这里答案就出来了,通过threadLocal.set(T)设置值时,实际上就是获取当前线程的ThreadLocalMap,每个线程都持有一个ThreadLocalMap对象,该map以threadLocal为key,value即为存储的值,保存进map中。
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(); }

同上,get方法实际上也是获取当前线程持有的threadLocalMap,以当前threadLoca作为key,从map中获取value。
总结:ThreadLocal实现线程局部变量的方法,就是每个线程都持有维护了一个threadLocalMap,在执行threadLocal对象的get和set方法时,都是获取当前线程的map对象,再以当前的threadLocal为key,进行value的操作,从而实现了线程局部变量的隔离

ThreadLocalMap底层实现:

每个线程中都持有了一个ThreadLocalMap用来存放线程局部变量,而ThreadLocalMap是为了实现ThreadLocal功能特意编写的map类,为什么不用现成的HashMap呢?
阅读ThreadLocalMap的源码,我们可以发现几个不同的点:
1、ThreadLocalMap中Entry的key设置为了弱引用。
这是为了防止key的内存泄漏,下面再仔细讲讲ThreadLocal的内存泄漏问题

static class Entry extends WeakReference> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

2、ThreadLocalMap解决hash冲突的方法。
ThreadLocalMap的hash算法为 threadLocalHashCode & (table.length - 1),而table.length指定为了2的整数次幂,因此等同于threadLocalHashCode % (table.length - 1)。
可以看到通过hash算法定位到数组下标,接着进行判断:若该entry的k为给定的key,则直接更新value;若k为空,说明该k被垃圾回收了,entry也该执行replaceStaleEntry进行清空;若不满足条件,则会获取数组entry为空下一个元素,跳出for循环。因此我们可知,ThreadLocalMap解决hash冲突的方法为定位到的数组下标往后移动。

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)]) { ThreadLocal<?> k = e.get(); if (k == key) {// k为给定的key,则直接更新value e.value = value; return; } if (k == null) {// k为空,说明该k被垃圾回收了,entry也该执行replaceStaleEntry进行清空 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

3、threadLocalHashCode为0x61c88647的整数倍。那为什么是这个魔法值呢?

private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }

我们已知table.length为2的整数幂,接下来以数组长度为16、32、64为例,探讨ThreadLocal的hash值为该魔法值的整数倍,发送hash冲突的情况:

public static void main(String[] args){ hash(16); hash(32); hash(64); } public static void hash(int length){ final int HASH_INCREMENT = 0x61c88647; int[] table = new int[length]; int hash = 0; for (int i = 0; i

结果分别为:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
同时将长度拓展到64、128、256…,都没有发生重复的情况。
因此我们可以得出结论:将ThreadLocal设置为该魔法值的整数倍,可以极大地减少存入ThreadLocalMap中的hash冲突的概率。 同时也不得不感慨作者的数学功底之深厚!

ThreadLocal的内存泄漏问题

刚刚我们有说到,ThreadLocalMap自定义的Entry继承了WeakReference,实际上便是将map中的key对threadLocal进行了弱引用。

弱引用介绍:弱引用是为了解决内存泄漏问题的,若一个对象只存在弱引用,在jvm垃圾回收时便会将该对象进行回收。场景:A a = new A(); B b = new B(); b.a = a; a = null; // 在这里只是将a的引用置为null,因为b.a对a还有强引用,a对象便还会存在内存中而不会被垃圾回收。解决办法:WeakReference wr = new WeakReference<>(a);b.wr = wr;//将b对a的引用改为弱引用

static class Entry extends WeakReference> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

引用关系如图:将key设置为弱引用,便可在threadLocal引用被置为null时,key对threadLocal的因为都为弱引用,jvm便可对threadLocal对象进行gc,从而防止threadLocal对象的内存泄漏。

但是!可以看到value的引用为强引用,若是线程能正常结束倒也还好说,线程结束了,map、entry、value的强引用都断开了,也就能被gc回收。但是通常情况下,因为线程的创建和销毁比较耗费性能,我们会使用诸如线程池的方法进行线程复用,这时候线程一直不被销毁,则很可能出现内存泄漏的问题。

解决办法

对于value内泄露的问题,ThreadLocal的开发者也注意到了,因此在调用threadLocal的get和set方法时,在碰上key为null的情况会执行replaceStaleEntry()方法清理调entry。而对于线程复用导致的内存泄漏问题,则可以在执行完毕后调用threadLocal.remove()方法手动清理。

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。