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). ——JDK1.7

翻译过来大概是这样的:ThreadLocal类是用来提供供线程内部使用局部变量。这种变量在多线程下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。

注意重点,它的作用是提供局部变量给线程内部使用。也就是说,它使用了一套机制保证:你new了一个变量threadLocal,在一个线程里,给threadLocal变量set一个别的线程无法访问使用的类型A的实例a,然后一段时间后,你可以从threadLocal变量中get出实例a,重点是这个threadLocal变量是可以跨线程的,在多个线程里做同样的事(set一个a1,a2…)否则跟在线程里直接new一个对象a就没有区别了。同时,JDK建议你把这个threadLocal变量设置为static,因为他需要在多线程中保持全局唯一,他也有能力在全局唯一的情况下,在多线程中提供局部变量。

总结一下,也就是说,只有你set进去的这个类型A实例小a是线程内部使用的变量,它才能保证小a是别的线程无法访问的。如果你给多个线程中set了同样的实例小a,那么你应该在类型A方法中采用各种锁来保证实例小a是多线程安全的。

ThreadLocal基本操作

构造函数

1
2
3
4
5
/**
* Creates a thread local variable.
*/
public ThreadLocal() {
}

构造函数没有入参。

initialValue()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the <tt>initialValue</tt> method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* <p>This implementation simply returns <tt>null</tt>; if the
* programmer desires thread-local variables to have an initial
* value other than <tt>null</tt>, <tt>ThreadLocal</tt> must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}

initialValue方法是该类作者提供给使用者供其产生局部变量的方法。

get()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 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)
return (T)e.value;
}
return setInitialValue();
}

get方法用来返回存储的变量,可以看到,如果没有获取到变量,最后会通过setInitialValue获取initialValue方法产生的变量。

set(T value)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 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方法比较简单,就是在一个线程对象的属性map中存入需要存储的变量,map的key就是这个threadLocal对象。

类关系

看上边的代码,其中牵扯map,thread,threadlocal,threadlocalmap等等各种类,也只能是走马观花的看看,具体还是需要了解类之间的关系。

upload successful

类关系如上图所示。ThreadLocal中有一个静态类ThreadLocalMap,ThreadLocalMap类中又有一个静态类Entry,Entry继承了WeakReference,这里不详细展开。其中Entry才是真正存储变量的类,其引用是ThreadLocal。ThreadLocalMap中有一个Entry[] table属性数组,table数组初始长度是16,会根据真正存储Entry实例数量的增加而X2倍扩展。

那一个Entry实例是放在table的哪个位置呢?是根据特定的hash算法&table.length求得的,如果其位置有Entry实例了,则会放到下一个位置里。所以获取变量时,是这么个流程:传入ThreadLocal实例自己到ThreadLocalMap实例方法入参中,然后方法会判断在table数组哪个位置上,然后判断是不是在下一个位置里等等,最后拿到Entry实例,取出变量返回。如果没存,那就返回null了。

现有的Thread类中有一个ThreadLocalMap属性,就是ThreadLocal中有一个静态类ThreadLocalMap类型的属性。这个属性在最开始实例化Thread时,是为null的。只有在ThreadLocal.set时,才会创建。

整个流程就是:如果你在一个线程中使用threadlocal去set一个变量,threadlocal会获取当前thread,然后如果当前thread的threadLocalMap属性为null则创建threadLocalMap并set(threadlocal,变量),threadLocalMap就会根据threadlocal的特定hash值给table特定位置创建一个Entry(threadlocal,变量)。

整个流程用图来表示:

upload successful

也验证了,其变量并不是同步共享的,只是通过同一个threadlocal实例可以在不同线程中获取不同线程的属性threadLocalMap中Entry的key为threadlocal的变量。

总结

适用的环境就是,每个线程都需要访问同一个类型的数据。而这个类型的实例根据不同线程其具体内容有所不同,这样就需要这么一个ThreadLocal了。

例如,有一些线程需要sql的connection。这时候,connection不能共享,则需要有一个static threadlocal1,自己去实现获取一个可用connection的方法initialValue(),然后在这些线程中使用threadlocal1的get方法,则保证了每个线程拿到的都只是局部的变量connection。

还有一个例子,在web中,每个用户请求都在一个线程里。怎么区分每一个用户?根据用户传来的cookie。这时候需要一个static threadlocal2,自己实现一个根据cookie获取用户信息bean的方法initialValue(),然后在线程中使用threadlocal1的get方法,获取用户信息bean。