什么是ThreadLocal?
关于ThreadLocal的知识网上有很多,但参差不齐很片面,看了很多博客后发现有一篇写的很全面客观,贴出来大家可以自行观看:http://www.iteye.com/topic/103804
下面讲一下我自己的理解:线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。ThreadLocal不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。
ThreadLocal的结构
首先我们来看一下ThreadLocal这个类的结构:
可以看到除了ThreadLocal的一些方法和变量之外,还有两个静态内部类:ThreadLocalMap和SuppliedThreadLocal,其中ThreadLocal是实现的关键,我们来看一下他的结构:
ThreadLocalMap里还有一个内部类Entry,其实每个线程存的值都在这里实现:
ThreadLocal实例
如果只是单纯的讲这个类难免枯燥,我们结合一个实例来讲;比如我们要在一个类中放一个String类型的字符串,让每个线程都能设置和获得不同的字符串,我们可以这样写:
public class Test {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static class Thread1 extends Thread{
@Override
public void run() {
//在新线程中设置值
threadLocal.set("a");
System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal.get());
}
}
public static void main(String[] args) {
//在主线程中设置值
threadLocal.set("main");
Thread1 thread1 = new Thread1();
thread1.start();
System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal.get());
}
}
首先在Test类中创建一个ThreadLocal对象,指定泛型类型为String,然后在Test类中创建一个静态内部类Thread1,在他的run方法中给ThreadLocal设置值,然后取出值。在mian方法中首先给ThreadLocal设置一个值,然后开启Thread1线程,运行结果如下:
线程:Thread[main,5,main]值:main
线程:Thread[Thread-0,5,main]值:a
可以看到主线程中设置的值和主线程中打印的值是相对应的,而新线程中设置的值和新线程中打印的值是相对应的,这就是ThreadLocal。
实例分析
下面我们对这个实例进行分析,看ThreadLocal是如何在不同的线程中取出不同的值的:
在我们new出一个Threadlocal实例时,ThreadLocal做了那些操作呢,我们看一下他的构造函数:
/**
* Creates a thread local variable.
*/
public ThreadLocal() {
}
我们可以看到他的构造函数是空的,也就是说当我们new一个实例时只是初始化了成员变量,我们看一下他的静态代码和成员变量:
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* 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.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
其中有只有一个threadLocalHashCode是在创建对象时初始化的,因此ThreadLocal实例的变量只有这个threadLocalHashCode,而且是final的,用来区分不同的ThreadLocal实例,至于为什么要这样一个变量后面会详细说。
接下来通过threadLocal.set(“main”)来设置值,这个方法很关键:
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(t)方法获取到与线程绑定的ThreadLocalMap实例,如果获取到的实例不为空,则通过ThreadLocalMap的set方法设置值,否则创建一个ThreadLocalMap实例。
我虽然只用几句话总结了这个方法,但其实里面的具体实现还是挺复杂的,我们来看一下 getMap(t)和createMap(t, value)这两个方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
getMap是返回一个ThreadLocalMap实例,而createMap是创建一个ThreadLocalMap实例,注意看创建的实例是放在那里的,t.threadLocals也就是Thread里的变量,我们看一下Thread类里的这个属性:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
这下就清楚了,我们保存值的时候,首先会通过Thread.currentThread()获取当前线程的实例,然后通过这个实例的getMap(t)方法获取线程里存放的ThreadLocalMap对象,如果当前线程里的ThreadLocalMap对象为空则创建一个实例放到该线程中,这样每个线程都拥有一个不同的ThreadLocalMap实例,而这个实例就是不同线程保存不同值的关键。搞清了这一点就基本了解ThreadLocal的原理了。
接着我们在看一下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);
}
首先初始化一个Entry数组,关于Entry其实他就相当于一个键值对,ThreadLocal是他的键(key),我们存放的值是他的值(values),原来我们的值最终是存到了这个Entry中了。然后通过ThreadLocal的threadLocalHashCode 属性计算出一个值,还记得这个变量吗,其实他的作用就是映射一个ThreadLocal在Entry数组中的位置,由于每个Threadlocal的threadLocalHashCode值是不一样的,这样当我们取值的时候,通过这个threadLocalHashCode 就可以找到我们存储的Entry对象的位置,这里也许有人会好奇了,为什么不直接通过key也就是ThreadLocal在数组中查询呢,hashMap不就是这样做的吗,其实hashMap底层也是使用的hash表来查询的,这样做的好处是查询快。我们通过threadLocalHashCode计算出了当前ThreadLocal的Entry对象在Entry数组中的位置,接着我们就把Entry对象初始化然后放到数组的对应位置。
为什么要用一个Entry数组呢?直接将值存到一个Entry对象中,然后ThreadLocalMap持有这个实例不就行了吗?刚开始我也在这里纠结了好久,但是仔细一想,如果我们线程要存两个值,但是一个线程只有一个ThreadLocalMap实例,这显然就不行了,所以要用Entry数组,将不同的值存到数组中,例:
public class Test {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
static class Thread1 extends Thread{
@Override
public void run() {
//在新线程中设置值
threadLocal.set("a");
threadLocal2.set(1);
System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal.get());
System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal2.get());
}
}
public static void main(String[] args) {
//在主线程中设置值
threadLocal.set("main");
threadLocal2.set(2);
Thread1 thread1 = new Thread1();
thread1.start();
System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal.get());
System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal2.get());
}
}
运行结果:
线程:Thread[main,5,main]值:main
线程:Thread[Thread-0,5,main]值:a
线程:Thread[main,5,main]值:2
线程:Thread[Thread-0,5,main]值:1
现在还差一个map.set(this, 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();
}
这个方法其实就是省去了初始化Entry数组的过程,直接通过threadLocalHashCode计算出了当前ThreadLocal的Entry对象在Entry数组中的位置。
这里我们先总结几个知识点:
- ThreadLocal的构造函数是的,在初始化的时候只初始化一个threadLocalHashCode变量,这是唯一标识当前ThreadLocal对象的
- ThreadLocalMap的实例是存放在Thread中的,ThreadLocalMap的构造函数接收两个参数:ThreadLocal和values
- 我们存的值最终是以键值对的形式存在Entry对象中的,ThreadLocal是键,values是值
- ThreadLocal进行set操作时会先获取当前线程实例中的ThreadLocalMap对象,然后将值设置到ThreadLocalMap中存储的Entry数组的Entry对象中。
- Entry对象存到Entry数组中的位置是由threadLocalHashCode计算得到的,这样做是为了节省查找时间,不懂的百度一下hash表
- Entry数组是为了实现一个线程存储多个值
下面我们看一下threadLocal.get()方法:
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方法也是要先获取当前线程的实例,然后在通过 getMap(t)方法获取到与线程绑定的ThreadLocalMap实例,如果获取到的实例不为空则通过这个ThreadLocalMap的getEntry()方法获取Entry实例,然后直接就获取了Entry实例中的值就可以了,如果ThreadLocalMap为空就初始化一个null值返回。
下面我们来详细分析一下get方法, getMap(t)不用讲了,和之前set里的一样,我们来看一下 map.getEntry(this)方法:
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);
}
这里就用到threadLocalHashCode 来获取Entry对象在Entry数组中的位置,只是简单的数学计算就可以完成,可以说根本不用时间,但是如果一个个取出Entry对象再一个个比较他们的key,这浪费的时间可想而知,取出Entry对象后判断是否为空,并且判断一下取出的key是否是和当前ThreadLocal一样 ,判断完成后返回Entry对象就ok了,至于如果Entry为空或者key值不一样的处理大家自己去看getEntryAfterMiss(key, i, e)的源码,我就不讲了,这种情况一般不会出现。
我们再看一下setInitialValue()方法:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
这个方法调用的条件是我们之前没有调用set方法给ThreadLocal设置值,这时是没有值能够取出来的,所以就调用一下该方法初始一个值,这个方法的第一行就是调用一个initialValue()方法,这个方法很重要,我们看一下他的实现:
protected T initialValue() {
return null;
}
返回空,没错因为我们没有值只能返回空,注意这个方法是可以重写的,我们可以自定义他的默认值:
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "defaultValues";
}
};
public static void main(String[] args) {
//在主线程中设置值
// threadLocal.set("main");
threadLocal2.set(2);
Thread1 thread1 = new Thread1();
thread1.start();
System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal.get());
System.out.println("线程:"+Thread.currentThread()+"值:"+threadLocal2.get());
}
线程:Thread[main,5,main]值:defaultValues
线程:Thread[main,5,main]值:2
线程:Thread[Thread-0,5,main]值:a
线程:Thread[Thread-0,5,main]值:1
在调用了initialValue()方法之后再次对ThreadLocalMap是否为空做一次判断,然后掉用 map.set(this, value)或者createMap(t, value)方法将我们初始化的值设置到Entry中。
这里再总结几个知识点:
- ThreadLocal进行get操作时也会先获取当前线程实例中的ThreadLocalMap对象,然后通过该对象获取Entry,进而获取到存储的值。
- ThreadLocal是有默认的值的,可以自定义也可以为空,默认的值也会存入Entry对象中。
到这里ThreadLocal的原理已经讲完了,可能有人会说这个东西根本没用到过,我们学他干嘛,我想说我们看源码更多的不是为了去使用它,如果只是ThreadLocal的使用我想几百字就解决了,我们是为了了解他的思想和解决问题的思路,这才是能提升我们的东西。