java中涉及到多线程共享数据的时候都必须要考虑线程安全的问题,今天来学习java中非常重要的辅助类——ThreadLocal。ThreadLocal可以在保证同一个实例被多个线程共同读写时依然能够保证数据的安全性。我们的项目中也有使用到了ThreadLocal,它主要是用在后台获取当前用户的微信信息。正好这段时间也在学习并发方面的知识,所以决定今天学习一下ThreadLocal的常用的方法,以及简单学习下它的底层原理。
1、ThreadLocal类
java JDK源码介绍中是这么说的
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).
大意即这个类提供了一个thread-local变量,thread-local变量和其他常规的变量不同之处在于,访问thread-local的每个线程都会有一个自己的独立初始化的变量副本,而ThreadLocal实例通常在希望与线程状态相关联的类中提供静态私有的域。
// 构造方法
ThreadLocal local = new ThreadLocal();
local.set("this is thread local ");
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
stringThreadLocal.set("this is a string generic thread local");
// 重写initialValue方法,默认返回的是null
ThreadLocal<String> genericThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "init thread local value";
}
};
// 类的静态方法
ThreadLocal staticThread = ThreadLocal.withInitial(() -> "static method init value");
java1.8以后ThreadLocal类增加了一个静态获取ThreadLocal实例的方法,只需要提供一个supplier函数即可。相当于创建一个有初始化值的实例。ThreadLocal类的方法并不算多,常用的估计也就set、get和remove这几个,下面看下各个方法的具体的实现:
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
我们看到ThreadLocal的set、get和remove方法又都用到了getMap方法,这个方法返回的是当前线程的threadLocals变量,这个变量类型是ThreadLocalMap。get方法先获取当前线程的threadLocals,如果这个值为空,则直接返回初始化值,否则的话会以当前的ThreadLocal实例作为key,获取与当前ThreadLocal实例关联的值。这个方法里面又涉及到到ThreadLocalMap类的组成和方法,这里暂不深究了。
set方法也是先获取当前线程的threadLocals变量,如果该变量为空,则调用createMap,否则调用ThreadLocalMap的set方法,关于涉及到的ThreadLocalMap方法这里暂时先不care了。下面看下createMap方法的代码:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
createMap方法创建了一个新的ThreadLocalMap实例,其以当前的ThreadLocal对象为key。并将新建的ThreadLocalMap实例赋值给当前线程的threadLocals变量。
通过上面的代码就可以发现,ThreadLocal和线程之间的关系就是通过thread类的成员变量threadLocals关联起来的,看下Thread类中的一段代码:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
注释的意思是threadLocals表示的属于这个线程的ThreadLocal的值,这个map,即ThreadLocal.ThreadLocalMap 由ThradLocal维护。
最后看下remove方法,remove方法其实调用的是ThreadLocalMap的remove方法。所以还是应该了解下ThreadLocalMap。
2、ThreadLocalMap类
其实前面说了这么多,最终还是落在了ThreadLocalMap身上,它才是保证各线程数据彼此隔离的关键,Thread类持有的成员变量threadLocals就是一个ThreadLocalMap实例。下面就看下ThreadLocalMap类的源码。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始化容量,必须为2的次幂
private static final int INITIAL_CAPACITY = 16;
// Entry数组,保存线程数据
private Entry[] table;
// table的大小
private int size = 0;
// table resize的阈值
private int threshold;
private void setThreshold(int len) {threshold = len * 2 / 3;}
private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}
private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private ThreadLocalMap(ThreadLocal.ThreadLocalMap parentMap) {...}
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {...}
private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {...}
private void set(ThreadLocal<?> key, Object value) {...}
private void remove(ThreadLocal<?> key) {...}
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {...}
private int expungeStaleEntry(int staleSlot) {...}
private boolean cleanSomeSlots(int i, int n) {...}
private void rehash() {...}
private void resize() {...}
private void expungeStaleEntries() {...}
}
根据ThreadLocalMap可以看到,它主要是通过一个变量名为table的Entry数组来存储线程数据,而每个Entry是以ThreadLocal实例作为key的。而我们看到Entry继承了WeakReference<ThreadLocal<?>>,意味着Entry持有的ThreadLocal是一个弱引用。Entry类的注释是这么写的:
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.
大概意思就是说ThreadLocalMap中的Entry继承了弱引用,使用它的主要引用字段作为key,这个引用是一个ThreadLocal对象。一旦key的值为null,即entry.get() == null,这就意味着这个key不再被引用了,这样这个key对应的Entry就会被从table数组中移除。我们知道弱引用的强度比软引用还要弱一些,弱引用关联的对象只能存活到下次垃圾回收之前,所以一旦ThreadLocal对象被垃圾回收器回收,那么对应的Entry也会被从table中移除,而成为“过时”的Entry,但是这个Entry并不会被垃圾回收,所以就可能引起内存溢出。关于有可能涉及到内存溢出的问题,可以看考下ThreadLocal可能引起的内存泄露。
下面主要看下ThreadLocal中使用到的关于ThreadLocalMap的方法,即getEntry、set和remove方法,下面看下这几个方法:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private void set(ThreadLocal<?> key, Object value) {
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();
}
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
虽然是三个方法,但是其中又会涉及到ThreadLocalMap的其他方法,这里就不再细述了,建议看一下源码最好。这里主要说一下这三个方法都用到一个运算,int i = key.threadLocalHashCode & (len-1)。通过当前ThreadLocal实例的threadLocalHashCode值和Entry数组,即table的长度len-1进行与运算,求出当前ThreadLocal对象为key的Entry在table中的下标,然后获取到对应的Entry对象,以便对其进行操作。我们先再次看一下ThreadLoca的部分源码:
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
ThreadLocal实例的threadLocalHashCode值是通过nextHashCode变量的getAndAdd方法得出来的,即ThreadLocal实例第一次获取的threadLocalHashCode值就是nextHashCode的值(默认0),之后获取的threadLocalHashCode都是在原有的基础上加上HASH_INCREMENT的值。getAndAdd方法是先返回再添加,且每次增加的数值大小是"0x61c88647"。
继续看int i = key.threadLocalHashCode & (len-1)这则运算,len的大小是2的次幂,这样就保证了len-1的低位都是1,即011...111。之所以使用"0x61c88647"作为自增的值,注解里面是这么解释的:
The difference between successively generated hash codes - turns implicit sequential thread-local IDs into near-optimally spread multiplicative hash values for power-of-two-sized tables.
大概意思是:生成连续的hash code的差异在于,为2次幂大小的table产成hash code时,将隐式有序的 thread-local id值转换成接近最优扩展的乘法。应该就是为了在大小为2次幂的table中生成均匀分布hash code,感兴趣的可以看下Why 0x61c88647?。
小结
虽然只是一个ThreadLocal类,但是其内部又涉及到了ThreadLocalMap和Entry两个类,开始以为东西不多,但是真的看起来发现内容并不算少,不过还在都是在一个文件里面,看起来还是很方便,而且很多方法自己并没有仔细的阅读。感觉看源码必须要看注解,这样才能理解作者的意图,也能方便自己快速的理解代码。以上只是自己的一点理解,如果有理解错误的地方还请指正。