设为首页 加入收藏

TOP

面试再也不怕问ThreadLocal了(四)
2023-08-06 07:49:47 】 浏览:84
Tags:ThreadLocal
Class = inner(); System.gc(); User user = testClass.get(); } private void inner() { User user = new User(); TestClass testClass = new TestClass(); testClass.set(user); return testClass; } class TestClass { ThreadLocal threadLocal = new ThreadLocal(); public void set(User user) { threadLocal.set(user); } public User get() { reteurn threadLocal.get(); } }

这里我们返回了TestClass,threadLocal对象就还被引用着,我们假设value如果是弱引用,那value在inner方法后就没有强引用了,gc后会被回收,会后再获取会拿到一个null,这显然是不合理的。
说到底,key设置为弱引用是为了防止内存泄漏,value不能设置为弱引用是因为如果key还被强引用着,value若是弱引用会被gc回收,下次就拿不到了。
从另一个方面说,开发人员处理的是value,key是java自己帮我们生成的,所以它要负责任,确保不会出现内存泄漏问题,而value是开发自己设置的,不需要时要手动remove,不然出现问题就是开发的锅啦。如果我忘记remove value,value泄漏是我的问题,但不能因此还多了一个key的泄漏,这个开发就不认了,为了避免这种纠缠不清问题,所以java作者将key设置为弱引用。

父线程/线程池传递ThreadLocal

如果在线程内,创建一个子线程,子线程还能访问到父线程的ThreadLocal吗?答案是不能的,但是从父子继承的角度来说,有时候需要能,所以Thread内部还有一个inheritableThreadLocals,它也是一个ThreadLocalMap。对应的也有一个InheritableThreadLocal,它继承了ThreadLocal。

     /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

在new Therad()创建子线程的时候有如下逻辑

 if (inheritThreadLocals && parent.inheritableThreadLocals != null)  
    this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);          

this是子线程,parent是父线程,也是当前线程,这里会判断父线程是否有inheritableThreadLocals,有就传递给子线程。
所以在父子线程场景下,传递ThreadLocal可以使用InheritableThreadLocal。

使用InheritableThreadLocal只能在第一次创建时把数据传递过去,后面主线程再改子线程也不会变化。对于使用线程池的情况,线程是复用的,如果希望子线程每次执行都能获取到主线程的ThreadLocal值,InheritableThreadLocal也无能为力了。例如日志跟踪traceid,每次执行主线程都会生成一个traceid,线程值每次执行,也都应该拿到最新的traceid,这样才能链路才能一致。
实现思路是自定义一个TtlRunnable继承Runnable,在执行run方法前,拷贝一下当前线程的值,在runnable.run执行前,将父线程的值拷贝到当前线程,这样每次执行都会做一次拷贝。

	public class TtlRunnable implements Runnable {

		private Runnable runnable;
		private HashMap<ThreadLocal, Object> ttlThreadLocals;

		public TtlRunnable(Runnable runnable) {
			this.runnable = runnable;
			//将当前线程的ThreadLocal拷贝一份
			ttlThreadLocals = copyCurrentThreadLocals();
		}

		@Override
		public void run() {
			//将父线程ThreadLocal拷贝到当前子线程
			copyParentThreadLocal2Current(ttlThreadLocals);
			runnable.run();
		}
	}

上面只是简单的实现思路,像spring cloud sleuth在处理traceid时思想也是类似的,当然实际还有很多东西要考虑,不过我们不用自己实现,阿里有一个TransmmittableThreadLocal可以直接使用,参见:transmittable-thread-local

ThreadLocal可以做哪些优化

能问到这里证明离offer已经不远了,基本很多面试官也不会问到这个层面。
回到ThreadLocal原理部分,它实际操作的是ThreadLocalMap,通过当前ThreadLocal的hashcode,计算Entry数组的下标,这个hashcode是new ThreadLocal()时通过一个全局的AtomicInteger累加0x61c88647得到。
跟hashmap的原理类似,通过hashcode计算下标,可能会出现hash冲突,hashmap使用链表+红黑树的方式解决hash冲突。而ThreadLocal使用线性探测法解决。
线性探测法的做法是,当出现hash冲突时,探测下一个位置,看看是否可以放入,可以就放入,否则继续往下一个位置探测。问题就出现在这里,当出现较多hash冲突时,相当于链表的遍历不断的探测,效率较低,可能ThreadLocal的作者认为ThreadLocal的设计上它不会存放太多数据吧。
那怎么优化呢?既然出现hash冲突影响效率,那干脆就不处理了,使用一个递增为1的AtomicInteger,每个ThreadLocal对应一个下标,这样就不会有冲突了,O(1)的查询速度,但是会占用较多空间,是一种空间换时间的思想。

实际这种做法就是netty中FastThreadLocal的实现,netty中提供了FastThreadLocal,FastThreadLocalMap,InternalThreadLocalMap,它们需要搭配使用,否则会退化为jdk的ThreadLocal。
每个FastThreadLocal都有一个递增唯一的index,放入InternalThreadLo

首页 上一页 1 2 3 4 下一页 尾页 4/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇MQ消息队列篇:三大MQ产品的必备.. 下一篇找出乱序数组第k大的数字(堆排序..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目