什么是ThreadLocal
成都一家集口碑和实力的网站建设服务商,拥有专业的企业建站团队和靠谱的建站技术,十年企业及个人网站建设经验 ,为成都上千多家客户提供网页设计制作,网站开发,企业网站制作建设等服务,包括成都营销型网站建设,成都品牌网站建设,同时也为不同行业的客户提供成都网站制作、成都做网站、外贸营销网站建设的服务,包括成都电商型网站制作建设,装修行业网站制作建设,传统机械行业网站建设,传统农业行业网站制作建设。在成都做网站,选网站制作建设服务商就选创新互联建站。
ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal, 每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。
实际应用
实际开发中我们真正使用ThreadLocal的场景还是比较少的,大多数使用都是在框架里面。最常见的使用场景的话就是用它来解决数据库连接、Session管理等保证每一个线程中使用的数据库连接是同一个。还有一个用的比较多的场景就是用来解决SimpleDateFormat解决线程不安全的问题,不过现在java8提供了DateTimeFormatter它是线程安全的,感兴趣的同学可以去看看。还可以利用它进行优雅的传递参数,传递参数的时候,如果父线程生成的变量或者参数直接通过ThreadLocal传递到子线程参数就会丢失,这个后面会介绍一个其他的ThreadLocal来专门解决这个问题的。
ThreadLocal api介绍
ThreadLocal的API还是比较少的就几个api
我们看下这几个api的使用,使用起来也超级简单
- private static ThreadLocal
threadLocal = ThreadLocal.withInitial(()->"java金融"); - public static void main(String[] args) {
- System.out.println("获取初始值:"+threadLocal.get());
- threadLocal.set("关注:【java金融】");
- System.out.println("获取修改后的值:"+threadLocal.get());
- threadLocal.remove();
- }
输出结果:
- 获取初始值:java金融
- 获取修改后的值:关注:【java金融】
是不是炒鸡简单,就几行代码就把所有api都覆盖了。下面我们就来简单看看这几个api的源码吧。
成员变量
- /**初始容量,必须为2的幂
- * The initial capacity -- MUST be a power of two.
- */
- private static final int INITIAL_CAPACITY = 16;
- /** Entry表,大小必须为2的幂
- * The table, resized as necessary.
- * table.length MUST always be a power of two.
- */
- private Entry[] table;
- /**
- * 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
这里会有一个面试经常问到的问题:为什么entry数组的大小,以及初始容量都必须是2的幂?对于 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 以及很多源码里面都是使用 hashCode &( -1) 来代替hashCode% 。这种写法好处如下:
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);
- }
set方法还是比较简单的,我们可以重点看下这个方法里面的ThreadLocalMap,它既然是个map(注意不要与java.util.map混为一谈,这里指的是概念上的map),肯定是有自己的key和value组成,我们根据源码可以看出它的key是其实可以把它简单看成是ThreadLocal,但是实际上ThreadLocal中存放的是ThreadLocal的弱引用,而它的value的话是我们实际set的值
- static class Entry extends WeakReference
> { - /** The value associated with this ThreadLocal. */
- Object value; // 实际存放的值
- Entry(ThreadLocal> k, Object v) {
- super(k);
- value = v;
- }
- }
Entry就是是ThreadLocalMap里定义的节点,它继承了WeakReference类,定义了一个类型为Object的value,用于存放塞到ThreadLocal里的值。我们再来看下这个ThreadLocalMap是位于哪里的?我们看到ThreadLocalMap 是位于Thread里面的一个变量,而我们的值又是放在ThreadLocalMap,这样的话我们就实现了每个线程间的隔离。下面两张图的基本就把ThreadLocal的结构给介绍清楚了。
接下来我们再看下ThreadLocalMap里面的数据结构,我们知道HaseMap解决hash冲突是由链表和红黑树(jdk1.8)来解决的,但是这个我们看到ThreadLocalMap只有一个数组,它是怎么来解决hash冲突呢?ThreadLocalMap采用「线性探测」的方式,什么是线性探测呢?就是根「据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置」。ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
- /**
- * 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);
- }
这种方式的话如果一个线程里面有大量的ThreadLocal就会产生性能问题,因为每次都需要对这个table进行遍历,清空无效的值。所以我们在使用的时候尽可能的使用少的ThreadLocal,不要在线程里面创建大量的ThreadLocal,如果需要设置不同的参数类型我们可以通过ThreadLocal来存放一个Object的Map这样的话,可以大大减少创建ThreadLocal的数量。伪代码如下:
- public final class HttpContext {
- private HttpContext() {
- }
- private static final ThreadLocal
- public static
void add(String key, T value) { - if(StringUtils.isEmpty(key) || Objects.isNull(value)) {
- throw new IllegalArgumentException("key or value is null");
- }
- CONTEXT.get().put(key, value);
- }
- public static
T get(String key) { - return (T) get().get(key);
- }
- public static Map
get() { - return CONTEXT.get();
- }
- public static void remove() {
- CONTEXT.remove();
- }
- }
这样的话我们如果需要传递不同的参数,可以直接使用一个ThreadLocal就可以代替多个ThreadLocal了。如果觉得不想这么玩,我就是要创建多个ThreadLocal,我的需求就是这样,而且性能还得要好,这个能不能实现列?可以使用netty的FastThreadLocal可以解决这个问题,不过要配合使FastThreadLocalThread或者它子类的线程线程效率才会更高,更多关于它的使用可以自行查阅资料哦。
下面我们先来看下它的这个哈希函数
- // 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
- private static final int HASH_INCREMENT = 0x61c88647;
- /**
- * Returns the next hash code.
- */
- private static int nextHashCode() {
- return nextHashCode.getAndAdd(HASH_INCREMENT);
- }
可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527.当我们使用0x61c88647这个魔数累加对每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂(数组的长度)取模,得到的结果分布很均匀。我们可以来也演示下通过这个魔数
- public class MagicHashCode {
- private static final int HASH_INCREMENT = 0x61c88647;
- public static void main(String[] args) {
- hashCode(16); //初始化16
- hashCode(32); //后续2倍扩容
- hashCode(64);
- }
- private static void hashCode(Integer length) {
- int hashCode = 0;
- for (int i = 0; i < length; i++) {
- hashCode = i * HASH_INCREMENT + HASH_INCREMENT;//每次递增HASH_INCREMENT
- System.out.print(hashCode & (length - 1));
- System.out.print(" ");
- }
- System.out.println();
- }
- }
运行结果:
- 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
- 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
- 7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0
不得不佩服下这个作者,通过使用了斐波那契散列法,来保证哈希表的离散度,让结果很均匀。可见「代码要写的好,数学还是少不了」啊。其他的源码就不分析了,大家感兴趣可以自行去查看下。
ThreadLocal的内存泄露
关于ThreadLocal是否会引起内存泄漏也是一个比较有争议性的问题。首先我们需要知道什么是内存泄露?
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
ThreadLocal的内存泄露情况:
- public static void main(String[] args) throws InterruptedException {
- ThreadLocal
threadLocal = new ThreadLocal<>(); - for (int i = 0; i < 50; i++) {
- run(threadLocal);
- }
- Thread.sleep(50000);
- // 去除强引用
- threadLocal = null;
- System.gc();
- System.runFinalization();
- System.gc();
- }
- private static void run(ThreadLocal
threadLocal) { - new Thread(() -> {
- threadLocal.set(new Long[1024 * 1024 *10]);
- try {
- Thread.sleep(1000000000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }).start();
- }
通过jdk自带的工具jconsole.exe会发现即使执行了gc 内存也不会减少,因为key还被线程强引用着。效果图如下:
在线程池的场景,程序不停止,线程一直在复用的话,基本不会销毁,其实本质就跟上面例子是一样的。如果线程不复用,用完就销毁了就不会存在泄露的情况。因为线程结束的时候会jvm主动调用exit方法清理。
- /**
- * This method is called by the system to give a Thread
- * a chance to clean up before it actually exits.
- */
- private void exit() {
- if (group != null) {
- group.threadTerminated(this);
- group = null;
- }
- /* Aggressively null out all reference fields: see bug 4006245 */
- target = null;
- /* Speed the release of some of these resources */
- threadLocals = null;
- inheritableThreadLocals = null;
- inheritedAccessControlContext = null;
- blocker = null;
- uncaughtExceptionHandler = null;
- }
InheritableThreadLocal
文章开头有提到过父子之间线程的变量传递丢失的情况。但是InheritableThreadLocal提供了一种父子线程之间的数据共享机制。可以解决这个问题。
- static ThreadLocal
threadLocal = new ThreadLocal<>(); - static InheritableThreadLocal
inheritableThreadLocal = new InheritableThreadLocal<>(); - public static void main(String[] args) throws InterruptedException {
- threadLocal.set("threadLocal主线程的值");
- Thread.sleep(100);
- new Thread(() -> System.out.println("子线程获取threadLocal的主线程值:" + threadLocal.get())).start();
- Thread.sleep(100);
- inheritableThreadLocal.set("inheritableThreadLocal主线程的值");
- new Thread(() -> System.out.println("子线程获取inheritableThreadLocal的主线程值:" + inheritableThreadLocal.get())).start();
- }
输出结果
- 线程获取threadLocal的主线程值:null
- 子线程获取inheritableThreadLocal的主线程值:inheritableThreadLocal主线程的值
但是InheritableThreadLocal和线程池使用的时候就会存在问题,因为子线程只有在线程对象创建的时候才会把父线程inheritableThreadLocals中的数据复制到自己的inheritableThreadLocals中。这样就实现了父线程和子线程的上下文传递。但是线程池的话,线程会复用,所以会存在问题。如果要解决这个问题可以有什么办法列?大家可以思考下,或者在下方留言哦。如果实在不想思考的话,可以参考下阿里巴巴的transmittable-thread-local哦。
总结
本文转载自微信公众号「java金融」,可以通过以下二维码关注。转载本文请联系java金融公众号。
网页名称:这4种ThreadLocal你都知道吗?
文章分享:http://www.csdahua.cn/qtweb/news5/294155.html
网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网