作用
ThreadLocal类用来提供线程内部的局部变量。一般ThreadLocal实例都是private static类型的。他会为每个线程提供一个独立的变量副本,解决了变量并发访问的冲突问题。ThreadLocal提供给各个线程的变量都是独立的,不会相互影响,是线程隔离的。
结构
现在的ThreadLocal是在每个子线程中维护一个TheadLocalMap,key保存的是TheadLocal,value是线程中需要用到的副本变量。然而早先的ThreadLocal是在ThreadLocal下维护TheadLocalMap,然后map中存的key是Thead,value是线程中需要用到的副本变量。
新的设计的好处:
1.每个Map存储的Entry数量变少。(因为新版的Entry数量取决于ThreadLocal的数量,而老版的Entry数量取决于Thread的数量,一般情况下ThreadLocal数量小于Thread数量)
2.当线程销毁的时候,ThreadLocalMap也会被回收,可以减少内存使用
核心方法
- protected T initialValue() 返回当前线程局部变量的初始值
- public void set(T value) 设置当前线程绑定的局部变量
- public T get() 获取当前线程绑定的局部变量
- public void remove() 移除当前线程绑定的局部变量
set:获取当前线程,通过当前线程获取一个map,如果map为空,则为该线程创建一个map,然后我们给map赋值,key为TheadLocal的引用,value为赋给该线程的局部变量。
get:获取当前线程,通过当前线程获取一个map,如果map不为空,则以ThreadLocal的引用作为key在map中获取对应的Entry e,如果e不为空,则get返回e.value。如果map为空或者Entry为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为key,value创建一个Map,get返回value。
remove:获取当前线程,通过当前线程获取一个map,如果获取的map不为空,则移除当前ThreadLocal对象对应的Entry。
initialValue:返回该线程局部变量的初始值。当我们使用ThreadLocal,未调用set方法先调用了get方法时会执行initialValue为线程创建一个map,因为map不为空了,所以之后再调用get就不会再执行initialValue。initialValue默认返回null,如果我们想要修改初始值,可以重写此方法。
ThreadLocalMap内部结构
TheadLocalMap内部有一个table是一个Entry类型的数组,数组的初始容量是INITIAL_CAPACITY,也有扩容阈值。Entry的key只能是ThreadLocal对象,这个Entry是继承自WeakReference<ThreadLocal<?>>,那也就是说key(ThreadLocal)是弱引用,这样可以将ThreadLocal对象的生命周期和线程的生命周期解绑。
ThreadLocalMap的hash相关
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
注:上面的AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用。
这里获取到当前值会加上HASH_INCREMENT,这个HASH_INCREMENT的值和斐波那契数列(黄金分割数)有关,其目的是为了让哈希码能均匀的分布在2的n次方的数组里,可以减少hash冲突。
TheadLocalMap中通过 hashcode& (size - 1) 确定Entry在数组中的位置,这个方式在size为2的n次方时等价于 hashCode % size,但是却更加高效。而且2的n次方减1的二进制每一位都是1,这也能减少hash冲突的发生,因此TheadLocalMap的容量是2的整次幂。
ThreadLocalMap中解决Hash冲突采用的是线性探测法。比如计算key的hash是5但是table[5]上已经有值,并且key与当前key不同,则会去table[6]上查看,如果还是被占用则去table[7]...,如果超出数组则从table[0]到table[1]....,依次往下找,直到发现一个table上为空或者table上key与当前key相等,将值放入。
ThreadLocal的内存泄漏
TheadLocalMap中的Entry下的key是弱引用,当gc来的时候key会变成null,但是(当前线程->Map->Entry)的这条强引用链还在,此时value和Entry是不会被回收的,然后key却成了null,那么这个value将无法被使用,从而导致内存泄漏。:
避免内存泄漏的两种方式:
- 使用完ThreadLocal,调用它的remove方法删除对应的Entry
- 使用完ThreadLocal,当前线程也随之结束
既然map的key使用弱引用无法解决内存泄漏为什么还要使用呢?
因为ThreadLocalMap的set/getEntry方法中,会对key是否为null进行判断,如果为null则会将value也置为null。这么一来,当用完ThreadLocal,而当前线程还在运行下,就会在我们忘记调用remove方法情况下多一层保障。
ThreadLocal和synchronized的区别
synchronized的同步是以时间换空间,只提供一个变量,然后不同线程排队访问。而ThreadLocal采用 空间换时间的方式。为每一个线程提供了一份变量的副本,各个线程操作这个变量互不影响。synchronized注重同步访问资源,而ThreadLocal是使每个线程的数据相互隔离。相比之下,ThreadLocal在高并发下有更好的性能。
使用举例
public static void test() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Person p = PersionManager.getPerson();
System.out.println("a1="+p.getName());
p.setName("小鱼");
System.out.println("a2="+p.getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("a3="+p.getName());
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
Person p = PersionManager.getPerson();
System.out.println("b1="+p.getName());
p.setName("小李");
System.out.println("b2="+p.getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b3="+p.getName());
}
});
t2.start();
}
class PersionManager{
static ThreadLocal<Person> threadLocal = new ThreadLocal<>();
public static Person getPerson(){
Person person = threadLocal.get();
if(person == null){
System.out.println("p null");
person= new Person("小明", "男");
threadLocal.set(person);
}else{
System.out.println("p not null");
}
return person;
}
}
class Person {
private String name;
private String sex;
public Person(String name, String sex) {
this.name = name;
this.sex = sex;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
上面的案例输出为:
p null
p null
b1=小明
b2=小李
a1=小明
a2=小鱼
b3=小李
a3=小鱼
由上面输出可知,if(person==null)进入了两次,说明ThreadLocal为每个子线程都创建了一个Person。