ThreadLocal类
虽然这个不是标准的数据结构,但是在java中还是挺重要的结构类。所以需要好好了解一下
使用
该类的使用非常简单,大概就下面的两个操作。
static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set()
sThreadLocal.get()
sThreadLocal.remove()
原理
ThreadLocal存在一个静态内部类ThreadLocalMap。当调用set的时候,首先获取当前线程对象t。然后线程t中存在一个成员变量threadLocals (指向ThreadLocal的静态内部类ThreadLocalMap),默认为null。 //如果存在ThreadLocalMap就直接set,没有则创建ThreadLocalMap并set。 然后将ThreadLocal对象作为key,用户的值作为value值。放入map中。
其实具体是采用ThreadLocal的threadLocalHashCode属性作为key。该属性是被final修饰的int不可变属性。所以可以唯一确定一个ThreadLocal对象。
但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。
总的来说其原理主要是通过为每个Thread 维护一个属于自己的map。然后通过不同的ThreadLocal对象作为key来存取不同的value值。也就是说再多线程环境中,对于同一个ThreadLocal对象,当处于不同的线程中时,对应的value值是不一样的。因为线程不一样->map不一样。
可能存在的问题
- 对于引用类型的共有变量。我们都知道引用类型传递的是内存地址的副本。通过该地址可以直接修改引用对象。因此如果共有变量是引用类型的话,在加入ThreadLocal的时候需要重新new或者clone。
例如:
package ztext;
/**
* @author xgj
*/
public class MyTest {
ThreadLocal<Long> longLocal = new ThreadLocal<>();
ThreadLocal<String> stringLocal = new ThreadLocal<>();
Long along = Long.valueOf(1000);
public void set() {
along+=100;
longLocal.set(along);
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final MyTest test = new MyTest();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(() -> {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
});
Thread thread2 = new Thread(() -> {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
的运行结果如下:
也就是说对于共有变量Long,并没有起到隔离作用
如果改为:
package ztext;
/**
* @author xgj
*/
public class MyTest {
ThreadLocal<Long> longLocal = new ThreadLocal<>();
ThreadLocal<String> stringLocal = new ThreadLocal<>();
Long along = Long.valueOf(1000);
public void set() {
Long blong = along+100;
longLocal.set(blong);
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final MyTest test = new MyTest();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(() -> {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
});
Thread thread2 = new Thread(() -> {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
结果:
- 内存泄露
下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:
如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。
- 源代码策略
- 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。 - 但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
- 使用策略
- 使用ThreadLocal,建议用static修饰 static ThreadLocal headerLocal = new ThreadLocal();
- 使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
其他
初始容量16,负载因子2/3,解决冲突的方法是再hash法,