一、什么是ThreadLocal
ThreadLocal被大多数人叫线程本地变量,ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。
二、使用ThreadLocal与ThreadLocal是如何实现的
下面看一段代码例子。
public class ThreadLocalDemo {
ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();
public String get() {
return stringThreadLocal.get();
}
public void set() {
stringThreadLocal.set("线程id:"+Thread.currentThread().getId()+",线程名:"+Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
final ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
//为threadLocal赋值
threadLocalDemo.set();
//输出
System.out.println(threadLocalDemo.get());
Thread thread = new Thread(){
@Override
public void run() {
//在新的线程中为threadLocal赋值
threadLocalDemo.set();
//输出
System.out.println(threadLocalDemo.get());
}
};
thread.start();
thread.join();
//输出
System.out.println(threadLocalDemo.get());
}
}
如果stringThreadLocal就是正常的String变量的话在第一次输出的时候应该输出
线程id:1,线程名:main
第二三次输出的时候应该输出的都是新创建线程的线程id与线程名
那么我们来看一下实际结果
线程id:1,线程名:main
线程id:12,线程名:Thread-0
线程id:1,线程名:main
执行后我们发现第三次输出的还是主线程的id与线程名,也就是说ThreadLocal起到了作用,他在新的线程中创建了一个变量副本,而没有去修改主线程中的变量。
那么ThreadLocal是如何做到的呢,首先我们先看一下ThreadLocal提供的方法。
//get()是用来获取ThreadLocal在当前线程中保存的副本
public T get() { }
//set() 是用来设置当前线程中的变量副本
public void set(T value) { }
//remove() 用来清除当前线程中的变量副本
public void remove() { }
// initialValue() 这个方法是需要使用的时候实现,具体的作用会在讲解get方法时说到
protected T initialValue() { }
首先我们先来看看get()方法的实现。
/**
* 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) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
这段代码首先获取了当前线程,随后调用了一个叫做getMap()的方法,获取类一个类型为ThreadLocalMap的Map,随后通过传入this,获取到键值对,如果成功获取到键值对则返回value,如果没有获取到map或键值对e都会去调用setInitialValue()方法。
在概括的说明了get方法之后,我们从上到下看一下get方法中所调用的方法都干了些什么。
首先是getMap(),我们去看一下ThreadLocalMap是从哪里获得的。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
令人震惊的是他直接返回了当前线程的一个成员变量threadLocals,那我们再去看一下Thread类中的成员变量。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
在查看了成员变量threadLocals后我们发现Thread中的成员变量threadLocals就是一个ThreadLocalMap,而这个类型是一个ThreadLocal的内部类。
static class ThreadLocalMap {
/**
* 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.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap的Entry继承了WeakReference,并使用了ThreadLocal作为键值。
那么看完了getMap(),我们继续来看setInitialValue()
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
这里就用到了之前说过的需要自己进行实现的方法initialValue(),咱们先不管他只需要知道他给我们返回了一个泛型T就可以了,之后的步骤就很容易了解了,与之前说的一样先去获得当前线程然后getMap(),判断map是否为null,如果为null创建并存入一个一个键值对,如果不为null,直接向map中存入一套键值对。
看完整个setInitialValue()方法,大家会发现initialValue()返回的value实际上就是存入键值对时的value,那我们去看一下没有重写的情况下initialValue()返回的是什么。
protected T initialValue() {
return null;
}
emmmmm,就是孤零零的一个null。
按照我们刚才的理解如果不事先进行set(),直接get()的话应该会返回给我们一个null,如果我们自己重写了initialValue()方法那么就会按照我们重写的方法返回给我们对应的对象,那现在我们来试一试。
首先我们去掉所有的set()执行一遍试试,按照预期的想法他应该会输出null。
public class ThreadLocalDemo {
ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();
public String get() {
return stringThreadLocal.get();
}
public void set() {
stringThreadLocal.set("线程id:"+Thread.currentThread().getId()+",线程名:"+Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
final ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
//为threadLocal赋值
//threadLocalDemo.set();
//输出
System.out.println(threadLocalDemo.get());
Thread thread = new Thread(){
@Override
public void run() {
//在新的线程中为threadLocal赋值
// threadLocalDemo.set();
//输出
System.out.println(threadLocalDemo.get());
}
};
thread.start();
thread.join();
//输出
System.out.println(threadLocalDemo.get());
}
}
下面看一下执行结果:
null
null
null
嗯,和预期的结果一样都是null,那下面我们来重写initialValue()方法试一下。
ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "线程id:"+Thread.currentThread().getId()+",线程名:"+Thread.currentThread().getName();
}
};
看一下执行结果:
线程id:1,线程名:main
线程id:12,线程名:Thread-0
线程id:1,线程名:main
和分析的结果一样,重写了initialValue()方法后,不需要我们去set()直接调用get()方法也能获取到我们想要的内容。
三、withInitial(Supplier<? extends S> supplier)
如果你不愿意去重写initialValue()方法,java8也为你提供了新的方案,通过函数式编程的方式来生成一个ThreadLocal,多说无益,直接修改一下我们的例子。
ThreadLocal<String> stringThreadLocal = ThreadLocal.
withInitial(()->"线程id:"+Thread.currentThread().getId()+",线程名:"+Thread.currentThread().getName());
修改后执行结果:
线程id:1,线程名:main
线程id:12,线程名:Thread-0
线程id:1,线程名:main
与之前的代码一致。
四、总结
- 通过ThreadLocal创建的副本是保存在每个Thread中自己的成员变量threadLocals中的。
- ThreadLocalMap的键使用ThreadLocal对象是因为每个线程中可能有许多ThreadLocal变量。
- 如果不希望调用set()方法请重写initialValue()方法,或通过withInitial()创建ThreadLocal对象,否则直接调用get()方法返回为null。
- 如果想在get()之前不需要调用set()的话,必须重写initialValue()方法,或通过withInitial()创建ThreadLocal对象。