欢迎转载,转载请注明出处:深入理解Java ThreadLocal!
一、定义
我们知道多线程环境下访问共享资源的会产生冲突,一般情况下我们可以通过锁机制来限定单个线程访问共享资源,让其他线程等待,但是这种方式会导致阻塞。避免多线程对共享资源访问产生的冲突的根本解决方式就是不共享。Java提供了一种叫做ThreadLocal类,我们称它为线程本地存储,从名称上我们就能看出它不是线程,而是一个存储变量的类,这个类存存储的变量在不同的线程中有不同的副本。对于ThreadLocal保存的变量,在每个线程中都保存有共享变量的一个副本,从而互不干扰。下面我们就一起熟悉下ThreadLocal的使用以及其原理,如有不正确的地方还望指正
二、使用场景以及使用方法
1,使用场景:一般情况下,当某个变量是以线程为作用域且不同线程具有不同该变量的副本的时候,就可以采用ThreadLocal来存储该变量。
2,基本使用
说了那么多概念,我们还是通过具体的实例来说明ThreadLocal的使用吧。
public class ThreadLocalTest {
private static final int INIT_VALUE = 10;
private static ThreadLocal<Integer> sValue = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return Integer.valueOf(INIT_VALUE);
}
};
public int getValue(){
return sValue.get().intValue();
}
public void setValue(int value){
sValue.set(Integer.valueOf(value));
}
public static void main(String[] agrs){
for (int i = 0; i < 5; i++){
new TestThread(i).start();
}
ThreadLocalTest test = new ThreadLocalTest();
System.out.println("value form main thread: " + test.getValue());
}
private static class TestThread extends Thread {
private int value;
public TestThread(int initialValue){
value = initialValue;
}
@Override
public void run() {
super.run();
ThreadLocalTest test = new ThreadLocalTest();
System.out.println("value before set(in thread id = " + Thread.currentThread().getId() + "): " + test.getValue());
test.setValue(value);
System.out.println("value after set(in thread id = " + Thread.currentThread().getId() + "): " + test.getValue());
}
}
}
上面是我写的一个测试类,用于测试ThreadLocal 的基本使用。首先我们定义了一个ThreadLocal 对象,ThreadLocal对象是支持泛型的,我们测试的时候用的是Integer;然后我们重写了ThreadLocal的initialValue()方法,该方法用于初始化ThreadLocal存储的变量的初始值。其次,我们为测试类定义了两个方法setValue()和getValue(),用于改变和获取ThreadLocal保存变量的值。最后定义了一个内部线程类,用于测在不同线程中改变ThreadLocal保存的变量是否会相互干扰。对于ThreadLocal中变量的访问,我们只能通过它提供的set(T value)和get()方法。测试类的说明就这些了,我们来看下运行后结果
我们可以看到,在我们没有调用set()方法前,每个线程中从ThreadLocal中得到的值都是初始值——10,当调用set方法后改变变量的值后,get()方法得到的值就是改变后的值,并且每个线程中通过get()方法得到的值都是在此线程中调用set()修改的值;各个线程中的值互不干扰,这就是ThreadLocal的神奇所在;最后我们看到在主线程中,由于没有修改过ThreadLocal 中变量的值,通过get()方法得到的值就是它的初始值。
三,ThreadLocal源码解析
在了解ThreadLocal的使用场景和基本使用后,我们就来探索下ThreadLocal的神奇之处的原因吧,要想了解它的神奇的原因,阅读它的源码是逃不掉的,下面我们就结合ThreadLocal的源码一起学习下。通过看ThreadLocal类的源码,我们会发现该类对外提供了三个方法(除构造方法之外),分别是:set(T value)(设置当前线程中ThreadLocal中保存的变量的值)、get()(获得当前线程中ThreadLocal保存的变量的值,如果在当前线程中没有调用set()方法改变变量的值时,返回的就是我们initialValue()中返回的值,这个我们在前面的例子中已经验证了)、remove()(删掉通过set()方法设置的值,也就是说你调用set()方法改变了ThreadLocal变量的值后,再调用remove()方法,ThreadLocal中变量的值就为initialValue()设置的初始值,如果有设置的话)。接下来我们就通过研究这几个对外提供的方法来深入理解ThreadLocal。
- set(T value),此方法用于设置当前线程中ThreadLocal 中保存的变量的值,我们看下该方法的具体内容
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
首先得到当前线程,然后通过getMap()方法得到ThreadLocalMap对象,ThreadLocalMap是个什么呢,ThreadLocalMap是定义在ThreadLocal中的内部类,它是个只适用于保存线程本地值的自定义的hash map。在ThreadLocalMap中定义了一个静态类Entry,该类就ThreadLocal 存储value的具体实现,它的具体定义如下,
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
可以看到,它是通过key-value的形式存储对象的,key就是ThreadLocal对象,value就是ThreadLocal中要存储的对象。同时该类继承弱引用,关于弱引用的内容可以参考我这篇文章——Java 引用分类:StrongReference、SoftReference、WeakReference、PhantomReference 。
现在我们继续回到ThreadLocal 的set(T value)方法,ThreadLocalMap对象是通过getMap()方法获得的,实际具体实现是获得线程的公共变量threadLocals,这个变量的初始化也是在ThreadLocal内完成的。当ThreadLocalMap对象map为空时,就会创建ThreadLocalMap实例,我们看下具体实现
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//ThreadLocalMap的构造方法
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
可以看出线程的threadLocals变量是在ThreadLocal.createMap()方法中初始化的。然后我们看ThreadLocalMap(ThreadLocal,firstKey,Object firstValue)构造方法,先是初始化了Entry数组table的大小,然后根据ThreadLocal的threadLocalHashCode计算得到entry对象存放在table中的位置,最后设置Entry数组下次扩容的大小。接下来我们看下当在ThreadLocal.set(T value)中如果得到的ThreadLocalMap对象不为null时,调用的map.set(ThreadLocal threadLocal, T value)方法。
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();
}
在此方法中,我们先看for语句,实际上也是遍历Entry数组table,看ThreadLocal对象key是否存在于table中,如果存在就更新它对应的value,同时如果发现table中某个元素的key为null,就会将传来的参数key和value替代这个元素。如果在for循环中没有返回,就会根据参数key和value创建一个Entry对象,并存放在数组table中,最后判断要不要扩大数组的容量。
- get(),这个方法用于获得ThreadLocal 中保存的变量的值,如果没有通过set(T value)方法设置变量的值,那么得到的将是initialValue()方法中返回的值。下面我们看下该方法的具体实现
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方法中,还是通过ThreadLocalMap对象得到Entry对象e,然后通过e得到保存的对象value。ThreadLocalMap的getEntry(ThreadLocal l)方法就不展开了,有兴趣的同学可以去研究下。至于ThreadLocal的remove()方法也不讲解了,有兴趣的同学也可以看下源码。其实通过ThreadLocal 的源码我们会发现,ThreadLocal之所以能实现对不同线程保存共享变量的副本,实际上是通过他的内部类ThreadLocalMap来实现的,ThreadLocalMap类中是通过Entry数组的形式来保存ThreadLocal和要保存的共享变量的值。
四,总结
对于ThreadLocal保存的共享变量,能够保证各个线程中保存着共享变量的副本,从而互不干扰。ThreadLocal之所以能实现这种功能是因为当线程访问ThreadLocal的set()和get()方法时,会初始化或者得到Thread的ThreadLocal.ThreadLocalMap对象threadLocals,对ThreadLocal的操作实际上是对Thread的变量threadLocals进行操作。也就是说当你调用ThreadLocal的set(T value)时,这个value是保存在Thread.threadLocals变量里面的,当你调用ThreadLocal.get()方法时,实际上是从Thread.threadLocals这个变量中得到之前保存的值。总结一句话:线程对ThreadLocal对象的操作,实际上是对该线程中threadLocals变量的操作。这也就好解释为什么ThreadLocal保存的共享变量,各线程下互不干扰的原因了。