本文属于并发编程网多线程学习笔记系列。原文地址:http://ifeve.com/java-theadlocal/
以下为原文:
Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作。因此,尽管有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadLocal变量的引用,但是这两个线程依然不能看到彼此的ThreadLocal变量域。
常用方法:
1创建一个ThreadLocal变量:
private
ThreadLocal myThreadLocal =
new
ThreadLocal();
你实例化了一个ThreadLocal对象。每个线程仅需要实例化一次即可。虽然不同的线程执行同一段代码时,访问同一个ThreadLocal变量,但是每个线程只能看到私有的ThreadLocal实例。所以不同的线程在给ThreadLocal对象设置不同的值时,他们也不能看到彼此的修改。
2访问ThreadLocal变量:
一旦创建了一个ThreadLocal对象,你就可以通过以下方式来存储此对象的值:
1 | myThreadLocal.set( "A thread local value" ); |
也可以直接读取一个ThreadLocal对象的值:
1 | String threadLocalValue = (String) myThreadLocal.get(); |
get()方法会返回一个Object对象,而set()方法则依赖一个Object对象参数。
3.ThreadLocal泛型
为了使get()方法返回值不用做强制类型转换,通常可以创建一个泛型化的ThreadLocal对象。以下就是一个泛型化的ThreadLocal示例:
private
ThreadLocal myThreadLocal1 =
new
ThreadLocal<String>();
现在你可以存储一个字符串到ThreadLocal实例里,此外,当你从此ThreadLocal实例中获取值的时候,就不必要做强制类型转换。
myThreadLocal1.set("Hello ThreadLocal");
String threadLocalValues = myThreadLocal.get();
4.初始化ThreadLocal
我们可以通过ThreadLocal子类的实现,并覆写initialValue()方法,就可以为ThreadLocal对象指定一个初始化值。如下所示:
private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
@Override protected String initialValue() {
return "This is the initial value";
}
};
此时,在set()方法调用前,当调用get()方法的时候,所有线程都可以看到同一个初始化值。
demo:
上面的例子创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行run()方法,并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并且调用的set()方法被同步了,则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是一个ThreadLocal对象,因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。
****************************分割线,原文结束,学习笔记开始********************************************************
源码分析
好了,原文结束,我们来看下背后的源码是如何为每个线程创建一个变量的副本的。
在Thread中有一个threadLocals变量,类型为ThreadLocal.ThreadLocalMap
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
而ThreadLocalMap则是ThreadLocal的静态内部类,他是一个用来保存thread local 变量的自定义的hash map 。方法是私有的,不对外暴露,只通过threadlocal访问。(可以理解为thread有个hashmap保存key-value).
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取thread那个hashmap
if (map != null) {//非空,this为key,也就是threadlocal为key
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)//获取value
return (T)e.value;
}//初始化
return setInitialValue();
}
逻辑如下:一开始是取得当前线程,然后通过getMap(t)方法获取到一个ThreadLocalMap类型的map。
判断map是否为空,不为空则传入this获取到<key,value>键值对,继而获取value。
如果map为空,则调用setInitialValue方法返回value。
深入进去看下ThreadLocal具体方法实现:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
就是返回当前线程的threadLocals。也就是前面的thread.的内部hashmap。
static class ThreadLocalMap {
/**
* 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继承了WeakReference,并且使用ThreadLocal作为键值。关于弱引用,下面在介绍。先看看set具体方法实现:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
实现相对简单:获取当前线程。获取map,判断map不为空,就设置键值对,为空,再创建Map。键值对:以ThreadLocal类型的变量为key,参数为value。通过上面get\set方法,简单总结一下:
- 每个Thread内部保存了一个"threadlocalMap",key为ThreadLocal,这个线程操作了多少个ThreadLocal,就有多少个key
- 获取ThreadLocal变量的值,调用ThreadLocal.get(),底层是threadlocalMap.get(this);
- 设置ThreadLocal变量的值,调用ThreadLocal.set(T value),底层是threadlocalMap.set(this,value);
其中createmap的实现如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
接下来看看ThreadLocalMap与弱引用的关系。
那么WeakReference有什么作用?原谅我这菜鸟水平,之前没有使用过。网上看了noteless 的文章。
ThreadLocal很好地解决了线程数据隔离的问题,但是也引入了另一个空间问题,如果线程数量很多,如果ThreadLocal类型的变量很多,将会占用非常大的空间。而对于ThreadLocal本身来说,他只是作为key,数据并不会存储在它的内部,所以对于ThreadLocal ThreadLocalMap内部的这个Entity的key是弱引用。
实线表示强引用,虚线表示弱引用。
对于真实的值是保存在Thread里面的ThreadLocal.ThreadLocalMap的 threadLocals中,借助于内部的这个map,通过ThreadLocal变量的get,可以获取到这个map的真正的值,也就是说,当前线程中持有对真实值value的强引用。
而对于ThreadLocal变量本身,如下代码所示,栈中的变量与堆空间中的这个对象,也是强引用的:
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
不过对于Entity来说,key是弱引用。
我们使用的只是new了一个ThreadLocal对象,所以当用户定义的ThreadLocal对象不再使用之后,也就是堆与栈之间的从ThreadLocal Ref到ThreadLocal的箭头会断开,ThreadLocal对象及其指向的T对象都应该可以被回收。再回到ThreadLocalMap上面,上面的源码看到到这里的Entry类的k被做了弱引用,所以ThreadLocal对象的回收不会受到entry类的影响,(GC时会回收弱引用)但是这个时候会存在键为null的value还没有被清除,而对于线程来说,如果迟迟不结束,那么就会一直存在:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value的强引用,所以value迟迟得不到回收,就会可能导致内存泄漏 。
ThreadLocalMap的设计中已经考虑到这种情况,所以ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value,以set方法为例看看
private void set(ThreadLocal key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {//擦除过时的entry
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
遍历table,如果k==null,说明这条记录可以删除,就用新值替换旧的,以便回收内存空间。
一旦将value设置为null之后,就斩断了引用于真实内存之间的引用,就能够真正的释放空间,防止内存泄漏.
但是这只是一种被动的方式,如果这些方法都没有被调用怎么办?每次使用完ThreadLocal变量之后,执行remove方法。通过key的弱引用,以及remove方法等内置逻辑,减少了内存泄漏的机会。
总结一下:
1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量.
3)在进行get之前,必须先set,否则会报空指针异常;
如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
******************************************
应用场景
ThreadLocal的官方API解释为:
“该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。”
ThreadLocalMap并不是为了解决线程安全问题,而是提供了一种将实例绑定到当前线程的机制,类似于隔离的效果。
http://www.iteye.com/topic/103804lujh99 写道
总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点:
1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
可以这样理解:ThreadLocalMap跟线程安全不是一会事,绑定上去的实例也不是多线程公用的,而是每个线程new一份,这个实例肯定不是共用的,如果共用了,那就会引发线程安全问题,要考其他手段来解决。
大神还举了session的例子:
下面来看一个hibernate中典型的ThreadLocal的应用:
Java代码
- private static final ThreadLocal threadSession = new ThreadLocal();
- public static Session getSession() throws InfrastructureException {
- Session s = (Session) threadSession.get();
- try {
- if (s == null) {
- s = getSessionFactory().openSession();
- threadSession.set(s);
- }
- } catch (HibernateException ex) {
- throw new InfrastructureException(ex);
- }
- return s;
- }
参考: