目录
参考:
一、介绍:
ThreadLocal,叫做线程本地变量,也可以叫做线程本地存储,ThreadLocal为变量在每一个线程中都创建出自己的副本值,所以多个线程之间相互不影响。
Thread类的源码中,有一个 ThreadLocal.ThreadLocalMap
类型的成员变量 threadlocals
,用来存储与当前线程相关的所有ThreadLocal值。
ThreadLocalMap
是ThreadLocal的内部类,使用 Entry
结构来存储值,Entry的key是当前ThreadLocal变量,value是变量副本。
结构如下所示
Thread类中的threadLocals变量:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal类的内部类ThreadLocalMap:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
......
......
}
二、ThreadLocal常用方法源码
1. get
获取当前线程在这个threadLocal变量中的副本值。
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();
}
2. set
设置当前线程指定的副本值存入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);
}
3. remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
4. 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;
}
5. initialValue
protected T initialValue() {
return null;
}
6. getMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
三、创建副本流程
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap
类型的成员变量threadLocals
,它就是用来存储实际的变量副本的,键为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread类里面,threadLocals
为null,当通过ThreadLocal变量调用get()
方法或者set()
方法时,就会threadLocals
进行初始化,并且以当前ThreadLocal变量为键,以ThreadLocal要保存的变量副本为value,存到threadLocals
。
然后在当前线程里面,如果要使用副本变量,就可以通过get()
方法在threadLocals
里面查找。
下面通过一个例子来证明通过ThreadLocal能达到在每个线程中创建变量副本的效果:
public class TestThreadLocal2 {
//long类型线程本地变量
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
//string类型线程本地变量
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
stringThreadLocal.set(Thread.currentThread().getName());
}
public Long getLong() {
return longThreadLocal.get();
}
public String getString() {
return stringThreadLocal.get();
}
public static void main(String[] args) throws InterruptedException {
TestThreadLocal2 test = new TestThreadLocal2();
test.set();
System.out.println("threadId: " + test.getLong());
System.out.println("threadName: " + test.getString());
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
test.set();
System.out.println("threadId: " + test.getLong());
System.out.println("threadName: " + test.getString());
}
});
thread.start();
thread.join();
System.out.println("threadId: " + test.getLong());
System.out.println("threadName: " + test.getString());
}
}
运行结果:
threadId: 1
threadName: main
threadId: 13
threadName: Thread-0
threadId: 1
threadName: main
从这段代码的输出结果可以看出,在main
线程中和Thread-0
线程中,longThreadLocal保存的副本值和stringThreadLocal保存的副本值都不一样。
最后一次在main线程再次打印副本值是为了证明在main
线程中和thread
线程中的副本值确实是不同的。
如果把上边main方法中最开始的test.set()
代码去掉会怎么样呢,这样代码会出现空指针异常。
因为在上面的源码展示部分中,我们发现如果没有先set的话,即在map中查找不到对应的存储,那么调用get()
方法时则会通过调用setInitialValue()
方法返回,而在setInitialValue()
方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。这就是出现空指针异常的原因。
要解决也很简单,只需要重写initialValue()
方法:
//long类型线程本地变量
ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
return Thread.currentThread().getId();
}
};
//string类型线程本地变量
ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return Thread.currentThread().getName();
}
};
这样的话不调用set()
直接调用get()
。
四、总结
1)实际上,通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象?那是因为每个线程中可以有多个threadLocal变量,就像上面代码中的longThreadLocal和stringThreadLocal;
3)在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。