我们都知道Java中有一个ThreadLocal类,但是你知道它为什么会被设计出来么,那它究竟解决了什么问题,在什么场景下使用呢?
首先我们先设想一个问题
我在a方法中有一个变量,我想在b方法中打印,我该怎么做
public statis void a(){
String uuid="02ED1C90-F75B-4A06-89E7-EE0451F4BABC";
log.info("[{}] a 方法执行了",uuid);
}
public statis void b(){
String uuid=; //这里我改怎么获取到a方法中的变量uuid呢
log.info("[{}] b 方法执行了",uuid);
}
于是我们想到了第一种解决方案
a 方法调用b 方法,然后把这个变量传进去嘛
于是就大概就编程了这样
public statis void a(){
String uuid="02ED1C90-F75B-4A06-89E7-EE0451F4BABC";
b(uuid)
log.info("[{}] a 方法执行了",uuid);
}
public statis void b(String uuidFromA){
String uuid=uuidFromA; //这里我改怎么获取到a方法中的变量name呢
log.info("[{}] b 方法执行了",uuid);
}
问题解决了么,当然。那如果 c ,d ,f 等函数也要做这样的操作呢,你怎么做,每个方法都加一个参数么,而且这个参数很可能是跟业务无关的。
然后你就会发现你的getUserNameById(String uuid,int id) 里面多了一个叫 uuid的业务无关的传参,是不是很别扭呢。而且调用的地方也不一定就能获取到这个参数,会让调用者很困惑,明明只需要一个id字段就好了,而且你的函数名也写的是ById,干嘛还要我传一个无关的 uuid 过去,设计一点都不专业啊。
那好,于是我们就产生了第二种解决方案
全局变量
public static String uuid="02ED1C90-F75B-4A06-89E7-EE0451F4BABC"
public statis void a(){
b()
log.info("[{}] a 方法执行了",uuid);
}
public statis void b(){
log.info("[{}] b 方法执行了",uuid);
}
是不是感觉清爽多了,问题解决了么,当然!但是,可是,可但是,只要用到全局的静态变量,就要很小心,你要看看这个静态变量是不是在其他的类中或者其他service中也会是使用,如果是就要设置为 public 如果不是就要设置为 private,另外还要关注这个变量是不是能被其他地方修改,还是只是被读取,而不会并发修改等等之类的问题。此外你还要注意,这个static 的全局的变量一旦初始化就会永久留在内存中,注意是永久,除非你手动的释放,否则gc也很难回收它,至少我到目前学到的jvm没有回收静态变量的,因为静态变量是跟随类实例化而产生,跟随类的消亡而消亡的,我还没见哪个类加载之后过一点时间还会销毁的。
好,现在问题升级,如果这个uuid 要随线程产生而产生,随线程消亡而释放(节约空间吗,只在线程中使用,不会永久驻留在内存中)。如何来解决上面提到的问题。
于是自然而然的我们想到只需要在线程初始化的时候,在线程对象里面(没错,线程也是一个对象,而且是一个占用空间不小的对象,Java中特意封装了一个类叫Thread用以映射操作系统的一个线程,其实说到底还是进程)保存一个变量,这个变量可用在线程的任何地方都访问到。
于是我又灵光一闪,想到了一个方案,我可以改写 Thread 类,然后让Thread类里面包含一个 HashMap 结构,用来存储我先搞在线程上线文中传递的变量,于是就变成了这样的设计。
public class ContextThread extends Thread {
private final Map<Object, Object> locals = new HashMap();
public ContextThread(Runnable target) {
super(target);
}
public void putLocal(Object k, Object v) {
locals.put(k, v);
}
public Object getLocal(Object k) {
return locals.get(k);
}
}
class Main2 {
public static void main(String[] args) {
Object uuid = new Object();
ContextThread uuid0 = new ContextThread(() -> {
((ContextThread)Thread.currentThread()).putLocal(uuid, "12345");
say();
});
uuid0.start();
ContextThread uuid1 = new ContextThread(() -> {
((ContextThread)Thread.currentThread()).putLocal(uuid, "12345222222");
say();
});
uuid1.start();
}
public static void say() {
Object uuid = ((ContextThread)Thread.currentThread()).getLocal("uuid");
System.out.println(uuid);
}
}
我上面自定义的 ContextThread ,也能实现保存变量的目的, 然后 每个变量也只在 当前线程的上下文中有效。为什么JDK的作者们不在Thread里面放一个HashMap呢?你要说HashMap 不够线程安全,那可以用ConcurrentHashMap呀。重点就在于这样设计的话,如法做到变量的隔离,因为我们还可以写出这样的代码
class Main2 {
public static void main(String[] args) {
Object uuid = new Object();
ContextThread uuid0 = new ContextThread(() -> {
((ContextThread)Thread.currentThread()).putLocal(uuid, "12345");
say();
});
uuid0.start();
ContextThread uuid1 = new ContextThread(() -> {
((ContextThread)Thread.currentThread()).putLocal(uuid, "12345222222");
say();
});
uuid1.start();
//注意这里
ContextThread uuid3 = new ContextThread(() -> {
((ContextThread)Thread.currentThread()).putLocal(uuid, "12345222222");
say();
uuid0.putLocal(uuid, "1234");
});
uuid3.start();
}
public static void say() {
Object uuid = ((ContextThread)Thread.currentThread()).getLocal("uuid");
System.out.println(uuid);
}
}
注意 ContextThread uuid3 = ...
这个线程里面居然改动了 uuid0线程里面的uuid的值,变量不隔离了。而且我们不得不写putLocal(k,v)这样丑陋的代码。
那就只能在换个方式设计了,要求也就更明确了
- 本地变量必须做到线程隔离,也就是另个线程看不到其他线程里面同名的变量值。
- 本地变量的写法必须的优雅,语义必须得明确
- 要能保证线程安全,因为已经线程隔离了,就不会共享,自然就线程安全了。
所以Java的开发大牛们就提前在设计Thread类的时候设计出来了这个变量。所以,ThreadLocal就是解决这个问题。目的就是为了在各自的线程里面共享一个变量或几个变量,你要共享几个,你就创建几个ThreadLocal对象,这样的好处就是避免了创建一个线程外的对象所要考虑的并发问题了,因为threadLocal都只是某一个线程里面自己的变量了,随便你怎么取值,怎么修改都不会影响其他的线程里面的值。
我们看Thread 中 本地变量的实现代码的时候,我们先看这个地方,大概在181行
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
我们发现他是一个叫做ThreadLocal.ThreadLocalMap的一个数据类型,我们先不着急进到 ThreadLocal.ThreadLocalMap 这个类里面,我们先看一下这个变量threadLocals我们就知道,原来在每一个线程里面还有一个隐藏的私有变量叫 threadLocal ,他的类型是 一个类似map的东西。我们要在脑海中构建出这样一个常见,一个线程,里面有一个map。
然后我们在进入到ThreadLocal类。重点去看他的api 中的 get方法和set方法,至于为什么先看这个(也是别人教我的,没有为什么)
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
很明显,先获取当前线程,Thread.currentThread(); 这里获取到的是当前应用上下文所在的线程的对象实例,就是原原本本的当前线程。
然后 getMap(t) ,把当前线程t当参数传递进去了,获取的是当前线程,也就是上面我们脑海中构建的那个map结构。这里是关键,因为获取的是当前线程的,那么每次set的时候,其实都是操作的自己线程的map,而不会影响其他线程里面的map. 因为线程是隔离的,那线程里面的变量自然也是隔离的。
如果map没有实例化就实例化一个,如果实例化了,就把 k=this 和 v=value 添加到那个数组里面。回头取的时候,也一定是取当前线程的当前map中按照k=this取。
这里的this指的第我们自己定义的那个ThreadLocal 变量,不是线程也不是其他,这一点要注意一下。
所以我们可以想象一下,实际上根本不是 threadLocal这个对象里面存储了某个value,其实你操作的还是线程的一个内部对象,这个对象是一个map.底层是KV结构。而threadLocal 仅仅是在充当一个key 罢了,所以我们完全可以自己设计实现一个对象也同样实现类似的功能,然后放到我们自定义的线程对象中去。
所以,get方法,闭着眼睛也能想到,肯定是先获取当前线程对象,然后获取当前线程对象map,然后把threalLocal对象的应用作为key去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();
}
果然,无出其右。其他逻辑像 setInitialValue 就自己看吧,不在主流程里面。
这个是最基本的ThreadLocal. 如果细心一点我们可以看到 ThreadLocal 是有很多子类的,既然有继承了,那肯定不同的子类都实现不同的ThreadLocal的一些方法,他们之间是有差异的,例如 TerminatingThreadLocal 、InheritableThreadLocal 等等,说实话我只知道InheritableThreadLocal用在父子类继承上,没有实际用过,以后再研究吧。
ThreadLocal 内部实现
ThreadLocalMap 和HashMap的功能类似,但是实现上却有很大的不同:
HashMap 的数据结构是数组+链表
butThreadLocalMap的数据结构仅仅是数组
HashMap 是通过链地址法解决hash 冲突的问题
butThreadLocalMap 是通过开放地址法来解决hash 冲突的问题
HashMap 里面的Entry 内部类的引用都是强引用
butThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用
ThreadLocalMap的数据结构仅仅是数组的原因是因为其解决Hash冲突的方法决定的,采用的叫开发地址发的冲突解决方案。如果用拉链法,才会在遇到冲突的会后采用链表的结构,其他解决冲突的手段一般都是用数组。
解决冲突的手段包括 拉链法、开放地址发、再哈希 等。其实在hash也算是开放地址法的一种。就是我遇到冲突的时候我在hash一把,直到找到一个能够盛放值的位置位置。
为什么ThreadLocalMap 采用开放地址法来解决哈希冲突?
jdk 中大多数的类都是采用了链地址法来解决hash 冲突,为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式
链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。
开放地址法
这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。
链地址法和开放地址法的优缺点
链式地址法(HashMap)
优点:
处理冲突简单,且无堆积现象,平均查找长度短;也就是说遇到冲突我就网链表的头上添加,因此简单,只要hash函数设计合理,那就不会出现堆积,因此查找速度也快。
链表中的结点是动态申请的,适合构造表不能确定长度的情况;如果不停的向map里面存值,只是冲突越来越多,但是只要往链表里面加就行。
缺点:
指针占用较大空间时,会造成空间浪费。
开放地址法:(ThreadLocalMap)
优点:
当节点规模较少,或者装载因子较少的时候,使用开放地址法较为节省空间。
缺点:
容易产生堆积问题;
不适于大规模的数据存储;
插入时可能会出现多次冲突的现象,删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂;
总体来说拉链法比较简单因此Java中大都用此手段,但是在本地变量这种场景下,JDK的先辈们考虑的可能跟深远吧。
ThreadLocalMap 采用开放地址法原因
- ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table,关于这个神奇的数字google 有很多解析,这里就不重复说了
- ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低
ThreadLocalMap的Entity为什么要集成WeekReference,使其key被弱引用对象指向。