ThreadLocal类
维持线程封闭性除了Ad-hoc封闭和栈封闭,更规范的方法是使用ThreadLocal,这个类是java lang包下的一个类。他能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立地副本,因此get总能返回由当前执行线程在调用set时设置的最新值。
源码分析
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);//再拿到当前线程的ThreadLocalMap
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal和ThreadLocalMap :
其实ThreadLocalMap 就是线程线程自身的一个成员属性threadLocals的类型。也就是线程本地数据都存在这个threadLocals应用的ThreadLocalMap中。
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
再看看ThreadLocalMap :
它里面有个静态内部类Entry,构造方法有两个参数:一个是ThreadLocal对象,一个是需要隔离访问的变量。
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {//两个变量
super(k);
value = v;
}
}
再看看它和ThreadLocalMap的关系:
Entry对象存储在这个table数组中,数组的下标是threadLocal的threadLocalHashCode&(INITIAL_CAPACITY-1),因为数组的大小是2的n次方,那其实这个值就是threadLocalHashCode%table.length,用&而不用%,其实是提升效率。只要数组的大小不变,这个索引下标是不变的,这也方便去set和get数据。
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//用数组保存,可能有多个变量需要线程隔离访问
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
看到这里大家应该就明白了,每个线程自身都维护着一个ThreadLocalMap,用来存储线程本地的数据,可以简单理解成ThreadLocalMap的key是ThreadLocal变量,value是线程本地的数据。就这样很简单的实现了线程本地数据存储和交互访问。如图:
场景:
ThreadLocal对象通常用于防止对可变的但实例变量(Singleton)或全局变量进行共享。例如:在单线程应用程序中可能会维持一个全局的数据库连接,并在层序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。如:
private static ThreadLocal<Connection> conmect ionHolder = new ThreadLocal<Connection> (){
public Connection initialValue(){
return DriverManager . getConnection(DB URL) ;
}
};
public static Connection getConnection(){
return connectionHolder .get () ;
}
当某个频繁执行的操作需要一个临时对象,例如一个缓存区,而又同时希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。
ThreadLocal引发的问题
内存泄漏问题
因为其中ThreadLocalMap是弱引用,挡GC发生时会被回收,其中引用链如图
当ThreadLocal指向null时,没有任何强引用指向实例,所以ThreadLocal会被gc回收,所以就会出现key为空的Entry,对应的value也将无法被访问,便存在着内存泄漏,而只有当前thread结束后,current thread不在栈中,强引用断开,gc才能回收。所以最好的方法就是在ThreadLocal不用是,用remove()来清除数据。
而仔细看get() set() 源码,则会发现其中都调用了expungeStaleEntry()来清除Entry中key为空的数据,但是这是不及时的,还是会存在内存泄漏。只有remove()中显式调用了expungeStaleEntry()。
从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得 思考:为什么使用弱引用而不是强引用?
下面我们分两种情况讨论:
key 使用强引用:对ThreadLocal 对象实例的引用被置为 null 了,但是 ThreadLocalMap 还持有这个 ThreadLocal 对象实例的强引用,如果没有手动删除, ThreadLocal 的对象实例不会被回收,导致 Entry 内存泄漏。
key 使用弱引用:对 ThreadLocal 对象实例的引用被被置为 null 了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的 对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 都 有机会被回收。
比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可 以多一层保障。
因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。
总结
- JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
- JVM 利用调用 remove、get、set 方法的时候,回收弱引用。
- 当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。
- 使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了 value 可能造成累积的情况。
安全性问题
ThreadLocalMap 中保存的其实是对象的一个引用,这样的话,当有其 他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有 的对象引用所指向的同一个对象实例。
所以在存在多个对同一数据的写操作存在竞争时,容易引发安全性问题。