今天在电面网易游戏的时候,面试官问到了ThreadLocal的作用,感觉自己答的不好,只答除了作用,关于ThreadLocal底层的具体实现并没有说明白,于是赶忙看了下源码,从源码层次理解一下ThreadLocal的实现原理。
1. 首先说说ThreadLocal有什么作用?
ThreadLocal是为解决多线程环境下变量的并发访问提出的另外一种思路(相对于加锁来说)。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 该作用也是ThreadLocal所表达的含义,就是线程私有的变量。
对于该对象我们一般使用的只有四个方法:
//设置当前线程的线程局部变量的值。
void set(Object value);
//该方法返回当前线程所对应的线程局部变量。
public Object get();
//将当前线程局部变量的值删除,目的是为了减少内存的占用。该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
public void remove();
//返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
protected Object initialValue();
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个虚拟的Map(这里之所以说是一个虚拟的Map是因为这个Map本身不存在,只是对于每个线程Thread对象里面有一个ThreadLocalMap对象),ThreadLocalMap用于存储每一个线程的变量副本。虚拟Map中元素的键为线程对象,而值对应线程的变量副本ThreadLocalMap中的值。
2. ThreadLocal的一个简单使用实例来说明问题:
先给出实例的代码:
package thread;
/**
* Created by louyuting on 2017/2/14.
*/
public class ThreadLocalDemo {
//创建一个ThreadLocal变量, 然后通过构造器传入 包含 这个属性的对象ThreadLocalDemo
//通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "init->";
}
};
/**
* 设置值并返回设置之后的新值
* @param string
* @return
*/
public String addString(String string){
stringThreadLocal.set(string);
return stringThreadLocal.get();
}
/**
* 获取ThreadLocal中的值
* @return
*/
public String getString(){
return stringThreadLocal.get();
}
public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
//这里有3个线程, 每个线程都传入包含ThreadLocal变量的对象, 然后对 ThreadLocal类型的变量stringThreadLocal 操作
TestThread thread1 = new TestThread(demo);
TestThread thread2 = new TestThread(demo);
TestThread thread3 = new TestThread(demo);
thread1.start();
thread2.start();
thread3.start();
}
private static class TestThread extends Thread{
private ThreadLocalDemo threadLocalDemo;
public TestThread(ThreadLocalDemo threadLocalDemo) {
this.threadLocalDemo = threadLocalDemo;
}
@Override
public void run() {
for(int i=0; i<3; i++){
//在线程私有的副本里面 改变ThreadLocal变量的值
System.out.println("thread[" + Thread.currentThread().getName() + "]--->threadlocaldemo["
+ threadLocalDemo.addString( threadLocalDemo.getString() + "[local" + i +"]-" ) );
}
}
}
}
上面的代码ThreadLocalDemo是主类, 类里面包含一个ThreadLocal的变量,泛型里面定义变量是String类型的。 在main函数中首先创建了ThreadLocalDemo实例,和三个TestThread实例,并把ThreadLocalDemo对象通过构造器分别传入三个相同的线程中。
现在我们关心的就是在线程中我们做了什么,在TestThread的run()方法中我们执行了三次对ThreadLocal变量拼接字符串。
根据前面我们说的ThreadLocal的作用,在每个不同的线程中ThreadLocal是独立的,其余线程的操作不会影响到当前线程中ThreadLocal的值。所以运行的结果应该是三个线程最后输出的结果是一样的,并且都是各自线程做的改变。
我们看看运行结果:
thread[Thread-0]--->threadlocaldemo[init->[local0]-
thread[Thread-0]--->threadlocaldemo[init->[local0]-[local1]-
thread[Thread-0]--->threadlocaldemo[init->[local0]-[local1]-[local2]-
thread[Thread-2]--->threadlocaldemo[init->[local0]-
thread[Thread-2]--->threadlocaldemo[init->[local0]-[local1]-
thread[Thread-1]--->threadlocaldemo[init->[local0]-
thread[Thread-2]--->threadlocaldemo[init->[local0]-[local1]-[local2]-
thread[Thread-1]--->threadlocaldemo[init->[local0]-[local1]-
thread[Thread-1]--->threadlocaldemo[init->[local0]-[local1]-[local2]-
我们可以看到[Thread-0]、[Thread-1]、[Thread-2]最后都是 threadlocaldemo[init->[local0]-[local1]-[local2]-
所以每个线程的改变都是私有的,与其余线程无关。
3. 源码层次解析ThreadLocal的实现:
通过分析ThreadLocal的set(T value)
方法和 get()
方法来分析其实现原理。
我们先看看 set()方法的源码:
public void set(T value) {
//首先获取当前线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
首先获取当前执行set()函数的线程,然后调用
getMap(t);
来获取当前线程键值对应的ThreadLocalMap变量,前面已经说过了ThreadLocalMap是ThreadLocal的一个内部静态类,里面保存的就是ThreadLocal变量的value值。最开始我们说过了Thread里面有一个虚拟的Map,现在我们就可以说出这个虚拟Map的键值对的类型:
Key | Value |
---|---|
当前线程Thread对象 | ThreadLocalMap对象 |
那么这个虚拟键值对是怎么存储的呢?我们看看
getMap(t);
的实现:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
其实就是在每个Thread对象中都有一个threadLocals属性,这样就类似于一个虚拟的键值对。我们看看是不是在Thread对象中有这个属性。
在Thread类的源码中确实发现了一行:
ThreadLocal.ThreadLocalMap threadLocals = null;
现在关于ThreadLocal的虚拟Map就可以解释清楚了。下面继续看ThreadLocal.set(T t)的源码。下面就是判断ThreadLocalMap这个变量是否为空,如果不为空,就直接调用ThreadLocalMap.set()方法将新值存入,如果为空就创建新的ThreadLocalMap对象,存入值,并传递给Thread.threadLocals属性,下面我们可以看看ThreadLocal.createMap()函数的实现来验证:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
下面再分析一下get()方法
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();
}
基本的业务逻辑也是和set()方法一样,都是先获取当前线程,通过当前线程获取对应的ThreadLocalMap对象,再获取ThreadLocalMap的value, 如果ThreadLocalMap为空,就返回创建ThreadLocal时的初始值。
4. 小结
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。