ThreadLocal线程变量的底层原理
1. ThreadLocal是什么
ThreadLocal提供线程局部变量。这些变量与普通的变量不同之处在于,每个访问这种变量的线程(通过它的get或set方法)都有自己的、独立初始化的变量副本。
例如:private int a = 1;
private User user;
像上面定义的这两个变量在我们的每一个线程中都会有一个他的副本,当前的线程所独享,这样就可以避免在多线程并发执行过程中的数据相互干扰的问题;
2. ThreadLocal内部主要方法介绍
主要方法:
/**
* 返回当前线程对ThreadLocal变量的“初始值”
* 这个方法将在线程第一次访问变量(通过调用get方法)时被调用,如果之前已经调用过了就不会再调了
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
/**
* 设置当前线程的ThreadLocal变量的副本为指定的值
*
* @param value the value to be stored in the current thread's copy of this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* 返回当前线程的ThreadLocal变量副本的值
*
* @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();
}
/**
* 删除当前线程的ThreadLocal变量副本的值
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
数据结构:
在Thread类中定义了一个ThreadLocalMap类型的数组用于存储当前线程的变量,具体如何进行存储后面会进行详细讲解;
ThreadLocal.ThreadLocalMap threadLocals = null;
3. ThreadLocal使用方法和数据存储方式
threadLocal代码案例
public class TestThreadLocal {
public static void main(String[] args) {
final ThreadLocal<Person> tl = new ThreadLocal<Person>();
//thread1
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(tl.get());
}
}).start();
//thread2
new Thread(new Runnable() {
public void run() {
tl.set(new Person());
}
}).start();
}
static class Person{
String name = "zhansan";
}
}
案例讲解:
1.上面这段代码我们可以看到创建了一个ThreadLocal类型的对象,并将tl变量指向了这个ThreadLocal对象;
2.我们创建了两个线程thread1和thread2,并且在thread1中我们调用了tl.get(),在thread2中我们tl.set(new Person());通过运行程序我们发现thread1拿不到thread2保存的变量;
内部存储结构分析
ThreadLocal存储模型图
存储结构分析:
1.在我们要使用ThreadLocal线程变量时,一般需要先创建一个ThreadLocal对象,一般ThreadLocal对象需要在具体的线程中使用,例如代码中分别在thread1和thread2中进行了使用;
2.在每个线程创建时内部都会维护一个ThreadLocalMap类型的数组threadLocals,这个数组中存储类型为Entry类型;Entry对象的key是当前ThreadLocal对象的弱引用,value就是我们要存储的数值(对象);
注:如果无法理解,可以将理解为我们new ThreadLocal()对象在每个线程独有的ThreadLocalMap中做了Entry对象的key;当前线程在通过访问自己ThreadLocalMap中的这个Key可以获取线程私有变量;
4. ThreadLocal内部Entry对象的引用为什么要使用弱引用(面试点)
内存泄漏问题
1.从上图我们可以看见tl对ThreadLocal对象是强引用,Entry对象的key对ThreadLocal对象是弱引用,这里为什么要这样进行设置呢?
如果我们key对ThreadLocal对象也使用强引用会出现什么情况。将设当我们的tl对象使用后将其设置成tl=null,正常这时ThreadLocal对象在没有引用的情况下会被GC,但很不幸Entry中的key对它进行了引用,而且还是强引用,这样导致ThreadLocal对象永远无法被回收,除非线程结束,ThreadLocalMap被回收key的引用也没有了才能被回收,容易出现内存泄漏问题;
但如果我们使用弱引用,只要是GC就会将引用的对昂进行删除不会出现内存泄漏问题;
2.在使用弱引用后当tl=null时我们的ThreadLocal对象会被GC,但这时还会出现一个问题,若果Entry对象的key被回收了就会导致这个key对应的value在内存中找不到了,无法进行GC,也就是说这个value对象无法被回收,出现内存泄漏的情况;
上面我们分析了导致内存泄漏的两种情况,那么我们在开发中应该如何使用ThreadLocal变量才能保证安全呢,一般推荐使用以下的方式:
ThreadLocal<Person> tl = new ThreadLoacal<Person>();//创建一个ThreadLocal变量
tl.set(new Person());//使用ThreadLocal变量
.........
.........
.........
tl.remove();//主动将我们的ThreadLocalMap进行清空;