本文属于并发编程网多线程学习笔记系列。原文地址:http://ifeve.com/java-theadlocal/
以下为原文:
Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作。因此,尽管有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadLocal变量的引用,但是这两个线程依然不能看到彼此的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对象,因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。
****************************分割线,原文结束,学习笔记开始********************************************************
好了,原文结束,我们来看下背后的源码是如何为每个线程创建一个变量的副本的。
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) 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; }
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
跟猜测的一样,是ThreadLocal的内部类ThreadLocalMap,看下它的实现 <img src="http://img.blog.csdn.net/20160412103648467?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" /> <span style="font-size:18px;">可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。</span>
<p style="font-family: monospace; white-space: pre;"><span style="font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 14px; line-height: 28px;">关于弱引用,下面在介绍set方法时一并说明。</span><span style="font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 14px; line-height: 28px;">再看看set具体方法实现:</span></p>
<pre name="code" class="java"> 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。
其中createmap的实现如下:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }接下来看看ThreadLocalMap与弱引用的关系。
那么WeakReference有什么作用?原谅我这菜鸟水平,之前没有使用过。网上搜了下,是跟垃圾回收有关的。如果一个对象只有WeakReference引用它,那么这个对象就可能被垃圾回收器回收。
我们通常用的都是强引用,比如hashmap,这意味着即使作为key的对象已经不存在了(指没有任何一个引用指向它),也仍然会保留在HashMap中,在某些情况下(例如内存缓存)中,这些过期的条目可能会造成内存泄漏等问题。(这块我先这样理解,应该单独整理下测试下,比如hashmap与weakhashmap的对比。)
我们使用的只是new了一个ThreadLocal对象,所以当用户定义的ThreadLocal对象不再使用之后,ThreadLocal对象及其指向的T对象都应该可以被回收。再回到ThreadLocalMap上面,上面的源码看到到这里的Entry类的k被做了弱引用,所以ThreadLocal对象的回收不会受到entry类的影响,结合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) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
总结一下:
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/103804 lujh99 写道
大神还举了session的例子:
下面来看一个hibernate中典型的ThreadLocal的应用: