本文作为ThreadLocal类的第一篇文章,主要通过源码分析ThreadLocal类的实现原理,概括它的运行方式,并简单分析如何使用ThreadLocal。
了解源码的第一步应当优先解读类文件的顶部注释。
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
*
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // Atomic integer containing the next thread ID to be assigned
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // Thread local variable containing each thread's ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // Returns the current thread's unique ID, assigning it if necessary
* public static int get() {
* return threadId.get();
* }
* }
*
* Each thread holds an implicit reference to its copy of a thread-local
* variable as long as the thread is alive and the {@code ThreadLocal}
* instance is accessible; after a thread goes away, all of its copies of
* thread-local instances are subject to garbage collection (unless other
* references to these copies exist).
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
从顶部注释中,我们可以得到:ThreadLocal提供的变量与普通变量不同。ThreadLocal提供了线程的局部变量,每个线程都可以通过get()和set()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,从而在多线程的情况下实现了线程的数据隔离。
简要言之:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。
回想一下生活中的一些常见的例子,例如浏览器访问网站时,会根据我们访问的网站从浏览器本地缓存中寻找该网站对应的Cookie,并且将这个Cookie包含在请求头里发送过去,而不是使用者去浏览器缓存中寻找A网站的Cookie,从而避免了参数传递。并且不同的浏览器之间的数据相互不可见,例如谷歌浏览器缓存中存储的Cookie火狐浏览器是不可见的,当然也不可能火狐浏览器打开了A网站,然后在谷歌浏览器中找到A网站的Cookie并放在请求头里发过去,从而实现了数据隔离。
在这个场景下,浏览器充当了一个类似中介的角色,我们需要什么,例如我在谷歌浏览器打开了A网站,就去和谷歌浏览器这个“中介”说一声:“把A网站的Cookie给我”,然后就能拿到A网站的Cookie,而不是自己再去手动的一个一个寻找Cookie。
再举一个例子,我们生活中常用的丰巢快递柜。如果快递小哥把我们网购的快递放到了丰巢快递柜,那么我们去拿快递的时候,只要找到对应的快递柜,手机扫码,之后快递柜就会自动打开我们的快递存放的那个仓位的仓门,不需要我们去一个仓位一个仓位的寻找快递。如果找错了快递柜,相应的扫码之后就会直接给出提示。
(例子如果举的有不恰当之处,欢迎举正)
在多线程的编程中,ThreadLocal也正是充当了这个中介的角色,在多线程中调用ThreadLocal的get()和set()方法,就能根据当前的线程直接存放或者取出对应的值,无需在做额外的操作,在保证了线程间数据隔离的前提下避免了参数传递。
public class Main {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
for (int i=0; i<5; ++i) {
new Thread(new Runnable() {
@Override
public void run() {
try {
threadLocal.set(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + " is stored");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getId() + ":" + threadLocal.get());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}).start();
}
}
}
运行结果:
Thread-0 is stored
Thread-2 is stored
Thread-3 is stored
Thread-1 is stored
Thread-4 is stored
13:Thread-2
14:Thread-3
15:Thread-4
11:Thread-0
12:Thread-1
Process finished with exit code 0
想要知道ThreadLocal的实现原理,就要去阅读它的源码。本文以JDK1.8为例,分析ThreadLocal的源码。其中,get()和set()是ThreadLocal最常用的两个方法。
ThreadLocal设计的奇妙之处在于:ThreadLocal变量本身并不保存数据,数据都是存储在当前线程维护的ThreadLocalMap中。ThreadLocalMap作为一个静态内部类,定义在ThreadLocal类中,当前线程初始化并维护这个ThreadLocalMap,而ThreadLocal变量只是充当了一个key,需要取值时,ThreadLocal变量作为key(实际上key并不是ThreadLocal本身,而是它的一个弱引用)从线程私有的ThreadLocalMap中取值。由于每个线程的ThreadLocalMap都是私有的,所以ThreadLocal能很好的实现线程间的数据隔离。
由于ThreadLocal的操作都是基于ThreadLocalMap进行的,因此,在阅读ThreadLocal源码之前,我们先去看看ThreadLocalMap的实现。
先看看ThreadLocalMap的顶部注解。
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
从注解中,我们可以知道:
/**
* 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> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap哈希表中键值对的定义,key是ThreadLocal对象的弱引用,因此当key为不再被外部强引用时,对应的键值对就已经过期了,可以从哈希表中删除。ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:“Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value”永远无法回收,造成内存泄露。
虽然,JDK调用ThreadLocalMap的getEntry()函数或者set()函数,会清除这些null key的entry,但是还不够。因为可能后面不调用这些函数了,所以这种方式不可能任何情况都能解决。
因此,很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
如果线程迟迟无法结束,也就是ThreadLocal对象将一直不会回收,例如线程池的情况,那么也将导致内存泄漏。(内存泄露的重点)。
接下来看一下ThreadLocal最常见的几个方法。
/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
/**
* 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);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
根据注释,可以知道这个方法大概做了什么事:把这个线程局部变量中,当前线程副本的值设置为指定的值。
set()方法中涉及到了getMap()、set()、createMap()。
getMap():
/**
* 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(Thread类中定义的ThreadLocalMap变量名是threadLocals)。
set():
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be 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();
}
作用:根据哈希算法,在当前线程对象维护的ThreadLocalMap中找到对应的位置,并插入键值对(具体的运行原理在下一遍ThreadLocal的文章中再详细描述)。
createMap():
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
作用:在线程中维护的ThreadLocalMap 还未初始化(第一次调用set()方法)时触发,实例化一个ThreadLocalMap 对象赋给当前线程中的threadLocals变量,并插入第一个键值对。
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
根据注释,结合我个人的理解,可以知道:返回此线程局部变量的当前线程副本中的值。如果变量没有当前线程的值,则首先将其初始化为调用{@link#initialValue}方法返回的值。
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
作用:如果Thread对象维护的ThreadLocalMap不为null,则删除key对应的键值对。
JDK1.8源码